From dc4effea7d85d459ea04b1ecb2d9fac1c08210ec Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt Date: Wed, 29 Jan 2025 01:14:26 -0500 Subject: [PATCH 1/7] Added `StoreCache` --- .../Caching/Benchmarks.StoreCache.cs | 110 +++++++ benchmarks/Neo.Benchmarks/Program.cs | 15 +- src/Neo.IO/IKeySerializable.cs | 18 ++ src/Neo/Collections/Caching/StoreCache.cs | 301 ++++++++++++++++++ .../KeyValueSerializableEqualityComparer.cs | 39 +++ src/Neo/Neo.csproj | 1 + src/Neo/SmartContract/KeyBuilder.cs | 2 +- src/Neo/SmartContract/StorageKey.cs | 3 +- .../Collections/Caching/UT_StoreCache.cs | 61 ++++ 9 files changed, 540 insertions(+), 10 deletions(-) create mode 100644 benchmarks/Neo.Benchmarks/Collections/Caching/Benchmarks.StoreCache.cs create mode 100644 src/Neo.IO/IKeySerializable.cs create mode 100644 src/Neo/Collections/Caching/StoreCache.cs create mode 100644 src/Neo/Collections/KeyValueSerializableEqualityComparer.cs create mode 100644 tests/Neo.UnitTests/Collections/Caching/UT_StoreCache.cs diff --git a/benchmarks/Neo.Benchmarks/Collections/Caching/Benchmarks.StoreCache.cs b/benchmarks/Neo.Benchmarks/Collections/Caching/Benchmarks.StoreCache.cs new file mode 100644 index 0000000000..83cc5333c6 --- /dev/null +++ b/benchmarks/Neo.Benchmarks/Collections/Caching/Benchmarks.StoreCache.cs @@ -0,0 +1,110 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmarks.StoreCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; +using Neo.Collections.Caching; +using Neo.Persistence; +using Neo.SmartContract; +using System.Diagnostics; + +namespace Neo.Benchmark.Collections.Caching +{ + [MemoryDiagnoser] // Enabling Memory Diagnostics + [CsvMeasurementsExporter] // Export results in CSV format + [MarkdownExporter] // Exporting results in Markdown format + public class Benchmarks_StoreCache + { + private static readonly MemoryStore s_memoryStore = new(); + private static readonly StoreCache s_storeCache = new(s_memoryStore); + private static readonly DataCache s_dataCache = new SnapshotCache(s_memoryStore); + + private static byte[] s_data = []; + private static StorageKey s_key; + private static StorageItem s_value; + + [GlobalSetup] + public void Setup() + { + if (s_data.Length == 0) + { + s_data = new byte[4096]; + Random.Shared.NextBytes(s_data); + s_key = new StorageKey(s_data); + s_value = new StorageItem(s_data); + } + } + + [Benchmark] + public void TestStoreCacheAddAndUpdate() + { + s_storeCache.Add(s_key, s_value); + s_storeCache[s_key] = new(s_data); + } + + [Benchmark] + public void TestStoreCacheRemove() + { + s_storeCache.Add(s_key, s_value); + Debug.Assert(s_storeCache.Remove(s_key, out _)); + } + + [Benchmark] + public void TestStoreCacheGetAlreadyCachedData() + { + s_storeCache.Add(s_key, s_value); + Debug.Assert(s_storeCache.TryGetValue(s_key, out _)); + Debug.Assert(s_storeCache.ContainsKey(s_key)); + _ = s_storeCache[s_key]; + } + + [Benchmark] + public void TestStoreCacheGetNonCachedData() + { + s_memoryStore.Put(s_key.ToArray(), s_key.ToArray()); + Debug.Assert(s_storeCache.TryGetValue(s_key, out _)); + Debug.Assert(s_storeCache.ContainsKey(s_key)); + _ = s_storeCache[s_key]; + } + + [Benchmark] + public void TestDataCacheAddAndUpdate() + { + _ = s_dataCache.GetOrAdd(s_key, () => s_value); + _ = s_dataCache.GetAndChange(s_key, () => new(s_data)); + } + + + [Benchmark] + public void TestDataCacheRemove() + { + _ = s_dataCache.GetOrAdd(s_key, () => s_value); + s_dataCache.Delete(s_key); + } + + [Benchmark] + public void TestDataCacheGetAlreadyCachedData() + { + _ = s_dataCache.GetOrAdd(s_key, () => s_value); + _ = s_dataCache.GetAndChange(s_key); + Debug.Assert(s_dataCache.Contains(s_key)); + _ = s_dataCache[s_key]; + } + + [Benchmark] + public void TestDataCacheGetNonCachedData() + { + s_memoryStore.Put(s_key.ToArray(), s_key.ToArray()); + _ = s_dataCache.GetAndChange(s_key); + Debug.Assert(s_dataCache.Contains(s_key)); + _ = s_dataCache[s_key]; + } + } +} diff --git a/benchmarks/Neo.Benchmarks/Program.cs b/benchmarks/Neo.Benchmarks/Program.cs index 437da9ed94..96c919d313 100644 --- a/benchmarks/Neo.Benchmarks/Program.cs +++ b/benchmarks/Neo.Benchmarks/Program.cs @@ -10,13 +10,12 @@ // modifications are permitted. using BenchmarkDotNet.Running; -using Neo.Benchmark; -using Neo.Benchmarks.Persistence.Benchmarks; -using Neo.SmartContract.Benchmark; +using Neo.Benchmark.Collections.Caching; // BenchmarkRunner.Run(); -BenchmarkRunner.Run(); -BenchmarkRunner.Run(); -BenchmarkRunner.Run(); -BenchmarkRunner.Run(); -BenchmarkRunner.Run(); +//BenchmarkRunner.Run(); +//BenchmarkRunner.Run(); +//BenchmarkRunner.Run(); +//BenchmarkRunner.Run(); +//BenchmarkRunner.Run(); +BenchmarkRunner.Run(); diff --git a/src/Neo.IO/IKeySerializable.cs b/src/Neo.IO/IKeySerializable.cs new file mode 100644 index 0000000000..91f6ba23bb --- /dev/null +++ b/src/Neo.IO/IKeySerializable.cs @@ -0,0 +1,18 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IKeySerializable.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.IO +{ + public interface IKeySerializable + { + byte[] ToArray(); + } +} diff --git a/src/Neo/Collections/Caching/StoreCache.cs b/src/Neo/Collections/Caching/StoreCache.cs new file mode 100644 index 0000000000..c86e74b6e4 --- /dev/null +++ b/src/Neo/Collections/Caching/StoreCache.cs @@ -0,0 +1,301 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StoreCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.IO; +using Neo.Persistence; +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Neo.Collections.Caching +{ + public class StoreCache(IStore store) : ICollection>, IEnumerable>, IEnumerable, IDictionary, IReadOnlyCollection>, IReadOnlyDictionary, ICollection, IDictionary + where TKey : class, IKeySerializable + where TValue : class, ISerializable, new() + { + private static readonly ConcurrentDictionary> s_memoryCache = new(Math.Min(Environment.ProcessorCount, 16), 0, KeyValueSerializableEqualityComparer.Default); + + private readonly IStore _store = store ?? throw new ArgumentNullException(nameof(store)); + + /// + public TValue this[TKey key] + { + get + { + if (TryGetSync(key, out var value) == false) + throw new KeyNotFoundException(); + + return value; + } + set + { + TryAddSync(key, value); + } + } + + /// + public object this[object key] + { + get + { + if (TryGetSync(key as TKey, out var value) == false) + throw new KeyNotFoundException(); + + return value; + } + set + { + TryAddSync(key as TKey, value as TValue); + } + } + + /// + public int Count => s_memoryCache.Count; + + /// + public bool IsReadOnly => false; + + /// + public bool IsFixedSize => false; + + /// + public bool IsSynchronized => false; + + /// + public object SyncRoot => throw new NotSupportedException(); + + /// + public ICollection Keys => s_memoryCache.Keys; + + /// + ICollection IDictionary.Keys => ((IDictionary)s_memoryCache).Keys; + + /// + IEnumerable IReadOnlyDictionary.Keys => s_memoryCache.Keys; + + /// + public ICollection Values => s_memoryCache.Values.Select(GetTargetValue).ToArray(); + + /// + ICollection IDictionary.Values => s_memoryCache.Values.Select(GetTargetValue).ToArray(); + + /// + IEnumerable IReadOnlyDictionary.Values => s_memoryCache.Values.Select(GetTargetValue); + + /// + public void Add(TKey key, TValue value) + { + TryAddSync(key, value); + } + + /// + public void Add(KeyValuePair item) + { + TryAddSync(item.Key, item.Value); + } + + /// + public void Add(object key, object value) + { + if (key is TKey tKey && value is TValue tValue) + TryAddSync(tKey, tValue); + } + + /// + public void Clear() + { + s_memoryCache.Clear(); + } + + /// + public bool Contains(KeyValuePair item) + { + return s_memoryCache.ContainsKey(item.Key) || _store.Contains(item.Key.ToArray()); + } + + /// + public bool Contains(object key) + { + if (key is TKey tKey) + return s_memoryCache.ContainsKey(tKey) || _store.Contains(tKey.ToArray()); ; + return false; + } + + /// + public bool ContainsKey(TKey key) + { + return s_memoryCache.ContainsKey(key) || _store.Contains(key.ToArray()); + } + + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotSupportedException(); + } + + /// + public void CopyTo(Array array, int index) + { + throw new NotSupportedException(); + } + + /// + public bool Remove(TKey key) + { + return TryRemoveSync(key, out _); + } + + /// + public bool Remove(KeyValuePair item) + { + return TryRemoveSync(item.Key, out _); + } + + /// + public void Remove(object key) + { + if (key is TKey tKey) + TryRemoveSync(tKey, out _); + } + + /// + public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue value) + { + return TryGetSync(key, out value); + } + + /// + public IEnumerator> GetEnumerator() + { + return s_memoryCache.Select(s => new KeyValuePair(s.Key, GetTargetValue(s.Value))).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + IDictionaryEnumerator IDictionary.GetEnumerator() + { + return s_memoryCache.ToDictionary(key => key.Key, value => GetTargetValue(value.Value)).GetEnumerator(); + } + + private static TValue GetTargetValue(WeakReference valueRef) + { + if (valueRef.TryGetTarget(out var value)) + return value; + + throw new NullReferenceException(); + } + + private bool TryRemoveSync(TKey key, [NotNullWhen(true)] out TValue value) + { + if (s_memoryCache.TryRemove(key, out var valueRef)) + { + _store.Delete(key.ToArray()); + + return valueRef.TryGetTarget(out value); + } + else + { + if (_store.TryGet(key.ToArray(), out var rawValue)) + { + value = rawValue.AsSerializable(); + + _store.Delete(key.ToArray()); + + return true; + } + + value = default; + return false; + } + } + + private bool TryGetSync(TKey key, [NotNullWhen(true)] out TValue value) + { + if (s_memoryCache.TryGetValue(key, out var valueRef)) + { + if (valueRef.TryGetTarget(out value) == false && _store.TryGet(key.ToArray(), out var rawValue)) + value = rawValue.AsSerializable(); + return true; + } + else + { + if (_store.TryGet(key.ToArray(), out var rawValue)) + { + // We do want to catch exceptions for serializer. + // We want exceptions to be thrown. There is no + // fast way to check "rawValue" bytes to see if + // data is "TValue" type or "typeof(TValue)". + // + // NOTE: + // Another cache class or IStore can overwrite + // "rawValue" bytes in the Store. Making it NOT + // "typeof(TValue)" for a given key. + value = rawValue.AsSerializable(); + + return s_memoryCache.TryAdd(key, new(value, false)); + } + } + + value = default; + return false; + } + + private bool TryAddSync(TKey key, TValue value) + { + if (s_memoryCache.TryGetValue(key, out var valueRef)) + { + if (valueRef.TryGetTarget(out var oldValue) && ReferenceEquals(value, oldValue) == false) + { + // Update target for cache + valueRef.SetTarget(value); + + // Save to store + // + // NOTE: + // This method of sync can change the + // "value" serializable type. If two + // caching classes use the same key + // but different ISerializable classes + _store.Put(key.ToArray(), value.ToArray()); + } + + return true; + } + else + { + if (s_memoryCache.TryAdd(key, new(value, false))) + { + // Save to store + // + // NOTE: + // This method of sync can change the + // "value" serializable type. If two + // caching classes use the same key + // but different ISerializable classes + _store.Put(key.ToArray(), value.ToArray()); + + return true; + } + } + + return false; + } + } +} diff --git a/src/Neo/Collections/KeyValueSerializableEqualityComparer.cs b/src/Neo/Collections/KeyValueSerializableEqualityComparer.cs new file mode 100644 index 0000000000..355b7ddc6c --- /dev/null +++ b/src/Neo/Collections/KeyValueSerializableEqualityComparer.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// KeyValueSerializableEqualityComparer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.IO; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Neo.Extensions +{ + internal class KeyValueSerializableEqualityComparer : IEqualityComparer + where TKey : IKeySerializable + { + public static readonly KeyValueSerializableEqualityComparer Default = new(); + + public bool Equals(TKey x, TKey y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; + + return x.ToArray().AsSpan().SequenceEqual(y.ToArray().AsSpan()); + } + + public int GetHashCode(TKey obj) + { + if (obj is null) + throw new ArgumentNullException(nameof(obj)); + return obj.GetHashCode(); + } + } +} diff --git a/src/Neo/Neo.csproj b/src/Neo/Neo.csproj index 32af25af36..0c6537b6fa 100644 --- a/src/Neo/Neo.csproj +++ b/src/Neo/Neo.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Neo/SmartContract/KeyBuilder.cs b/src/Neo/SmartContract/KeyBuilder.cs index a752e89138..9c755bc0fd 100644 --- a/src/Neo/SmartContract/KeyBuilder.cs +++ b/src/Neo/SmartContract/KeyBuilder.cs @@ -20,7 +20,7 @@ namespace Neo.SmartContract /// /// Used to build storage keys for native contracts. /// - public class KeyBuilder + public class KeyBuilder : IKeySerializable { private readonly MemoryStream stream; diff --git a/src/Neo/SmartContract/StorageKey.cs b/src/Neo/SmartContract/StorageKey.cs index 9f2c334edf..442084fbcd 100644 --- a/src/Neo/SmartContract/StorageKey.cs +++ b/src/Neo/SmartContract/StorageKey.cs @@ -10,6 +10,7 @@ // modifications are permitted. using Neo.Extensions; +using Neo.IO; using System; using System.Buffers.Binary; using System.Runtime.CompilerServices; @@ -19,7 +20,7 @@ namespace Neo.SmartContract /// /// Represents the keys in contract storage. /// - public sealed record StorageKey + public sealed record StorageKey : IKeySerializable { /// /// The id of the contract. diff --git a/tests/Neo.UnitTests/Collections/Caching/UT_StoreCache.cs b/tests/Neo.UnitTests/Collections/Caching/UT_StoreCache.cs new file mode 100644 index 0000000000..2b2645d00b --- /dev/null +++ b/tests/Neo.UnitTests/Collections/Caching/UT_StoreCache.cs @@ -0,0 +1,61 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_StoreCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Collections.Caching; +using Neo.Extensions; +using Neo.Persistence; +using Neo.SmartContract; + +namespace Neo.UnitTests.Collections.Caching +{ + [TestClass] + public class UT_StoreCache + { + + [TestMethod] + public void TestAddAndGetSync() + { + using var memoryStore = new MemoryStore(); + var storeCache = new StoreCache(memoryStore); + + var expectedKey = new StorageKey([0, 1, 2, 3, 4, 5, 6]); + var expectedValue = new StorageItem([7, 8, 9, 0, 11, 12]); + + storeCache[expectedKey] = expectedValue; + + var actualStoreValue = memoryStore.TryGet(expectedKey.ToArray()); + var actualCacheValue = storeCache[expectedKey]; + + Assert.IsNotNull(actualStoreValue); + Assert.IsNotNull(actualCacheValue); + + CollectionAssert.AreEqual(expectedValue.ToArray(), actualStoreValue); + Assert.AreSame(expectedValue, actualCacheValue); + } + + [TestMethod] + public void TestStoreCacheGetNonCachedData() + { + using var memoryStore = new MemoryStore(); + var storeCache = new StoreCache(memoryStore); + + var expectedKey = new StorageKey([0, 1, 2, 3, 4, 5, 6]); + var expectedValue = new StorageItem([7, 8, 9, 0, 11, 12]); + + memoryStore.Put(expectedKey.ToArray(), expectedValue.ToArray()); + + Assert.IsTrue(storeCache.TryGetValue(expectedKey, out _)); + Assert.IsTrue(storeCache.ContainsKey(expectedKey)); + _ = storeCache[expectedKey]; + } + } +} From 27e4ebc1ddb52bf3106ed656aba77130fc4c2626 Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 29 Jan 2025 04:23:48 -0800 Subject: [PATCH 2/7] Update tests/Neo.UnitTests/Collections/Caching/UT_StoreCache.cs --- tests/Neo.UnitTests/Collections/Caching/UT_StoreCache.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Neo.UnitTests/Collections/Caching/UT_StoreCache.cs b/tests/Neo.UnitTests/Collections/Caching/UT_StoreCache.cs index 2b2645d00b..e15ffa5e72 100644 --- a/tests/Neo.UnitTests/Collections/Caching/UT_StoreCache.cs +++ b/tests/Neo.UnitTests/Collections/Caching/UT_StoreCache.cs @@ -20,7 +20,6 @@ namespace Neo.UnitTests.Collections.Caching [TestClass] public class UT_StoreCache { - [TestMethod] public void TestAddAndGetSync() { From 0406d34123dfedf584b7a0c72f58b411b3b85408 Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt Date: Wed, 29 Jan 2025 19:40:45 -0500 Subject: [PATCH 3/7] Updates and fixes --- .../Caching/Benchmarks.StoreCache.cs | 98 +++++++-------- .../Neo.Benchmarks/Neo.Benchmarks.csproj | 5 +- .../ByteArrayEqualityComparer.cs | 2 +- src/Neo/Collections/Caching/StoreCache.cs | 118 ++++++++++-------- src/Neo/Persistence/MemoryStore.cs | 12 +- 5 files changed, 124 insertions(+), 111 deletions(-) diff --git a/benchmarks/Neo.Benchmarks/Collections/Caching/Benchmarks.StoreCache.cs b/benchmarks/Neo.Benchmarks/Collections/Caching/Benchmarks.StoreCache.cs index 83cc5333c6..4354c1ae0a 100644 --- a/benchmarks/Neo.Benchmarks/Collections/Caching/Benchmarks.StoreCache.cs +++ b/benchmarks/Neo.Benchmarks/Collections/Caching/Benchmarks.StoreCache.cs @@ -10,101 +10,99 @@ // modifications are permitted. using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Order; using Neo.Collections.Caching; using Neo.Persistence; using Neo.SmartContract; +using Perfolizer.Mathematics.OutlierDetection; using System.Diagnostics; namespace Neo.Benchmark.Collections.Caching { - [MemoryDiagnoser] // Enabling Memory Diagnostics - [CsvMeasurementsExporter] // Export results in CSV format - [MarkdownExporter] // Exporting results in Markdown format + // Result Exporters + [MarkdownExporter] // Exporting results in Markdown format + // Result Output + [MinColumn, MaxColumn, MeanColumn, MedianColumn] // Include these columns + [Orderer(SummaryOrderPolicy.Declared, MethodOrderPolicy.Declared)] // Keep in current order as declared in class + // Job Configurations + [SimpleJob(RuntimeMoniker.Net90)] + [Outliers(OutlierMode.DontRemove)] + [GcServer(true)] // GC server is enabled for GitHub builds in `neo` repo + [GcConcurrent(true)] // GC runs on it own thread + [GcForce(false)] // DO NOT force full collection of data for each benchmark public class Benchmarks_StoreCache { - private static readonly MemoryStore s_memoryStore = new(); - private static readonly StoreCache s_storeCache = new(s_memoryStore); - private static readonly DataCache s_dataCache = new SnapshotCache(s_memoryStore); + private readonly MemoryStore _memoryStore = new(); + private readonly StoreCache _storeCache; + private readonly DataCache _dataCache; - private static byte[] s_data = []; - private static StorageKey s_key; - private static StorageItem s_value; + private readonly StorageKey _key; + private readonly StorageItem _value; - [GlobalSetup] - public void Setup() + public Benchmarks_StoreCache() { - if (s_data.Length == 0) - { - s_data = new byte[4096]; - Random.Shared.NextBytes(s_data); - s_key = new StorageKey(s_data); - s_value = new StorageItem(s_data); - } + _storeCache = new(_memoryStore); + _dataCache = new SnapshotCache(_memoryStore); + + var data = new byte[1024]; + new Random(0xdead).NextBytes(data); + + _memoryStore.Put(data, data); + _key = new(data); + _value = new(data); } [Benchmark] - public void TestStoreCacheAddAndUpdate() + public void TestStoreCacheAdd() { - s_storeCache.Add(s_key, s_value); - s_storeCache[s_key] = new(s_data); + _storeCache.Add(_key, _value); } [Benchmark] - public void TestStoreCacheRemove() + public void TestDataCacheAdd() { - s_storeCache.Add(s_key, s_value); - Debug.Assert(s_storeCache.Remove(s_key, out _)); + Debug.Assert(_dataCache.GetOrAdd(_key, () => _value) != null); } [Benchmark] - public void TestStoreCacheGetAlreadyCachedData() + public void TestStoreCacheUpdate() { - s_storeCache.Add(s_key, s_value); - Debug.Assert(s_storeCache.TryGetValue(s_key, out _)); - Debug.Assert(s_storeCache.ContainsKey(s_key)); - _ = s_storeCache[s_key]; + Debug.Assert(_storeCache.Update(_key, _value)); } [Benchmark] - public void TestStoreCacheGetNonCachedData() + public void TestDataCacheUpdate() { - s_memoryStore.Put(s_key.ToArray(), s_key.ToArray()); - Debug.Assert(s_storeCache.TryGetValue(s_key, out _)); - Debug.Assert(s_storeCache.ContainsKey(s_key)); - _ = s_storeCache[s_key]; + Debug.Assert(_dataCache.GetAndChange(_key, () => _value) != null); } [Benchmark] - public void TestDataCacheAddAndUpdate() + public void TestStoreCacheRemove() { - _ = s_dataCache.GetOrAdd(s_key, () => s_value); - _ = s_dataCache.GetAndChange(s_key, () => new(s_data)); + _storeCache.Remove(_key); } - [Benchmark] public void TestDataCacheRemove() { - _ = s_dataCache.GetOrAdd(s_key, () => s_value); - s_dataCache.Delete(s_key); + _dataCache.Delete(_key); } [Benchmark] - public void TestDataCacheGetAlreadyCachedData() + public void TestStoreCacheGet() { - _ = s_dataCache.GetOrAdd(s_key, () => s_value); - _ = s_dataCache.GetAndChange(s_key); - Debug.Assert(s_dataCache.Contains(s_key)); - _ = s_dataCache[s_key]; + Debug.Assert(_storeCache.TryGetValue(_key, out _)); + Debug.Assert(_storeCache.ContainsKey(_key)); + Debug.Assert(_storeCache[_key] != null); } [Benchmark] - public void TestDataCacheGetNonCachedData() + public void TestDataCacheGet() { - s_memoryStore.Put(s_key.ToArray(), s_key.ToArray()); - _ = s_dataCache.GetAndChange(s_key); - Debug.Assert(s_dataCache.Contains(s_key)); - _ = s_dataCache[s_key]; + Debug.Assert(_dataCache.GetAndChange(_key) != null); + Debug.Assert(_dataCache.Contains(_key)); + Debug.Assert(_dataCache[_key] != null); } } } diff --git a/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj b/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj index debd94430d..4439741d3b 100644 --- a/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj +++ b/benchmarks/Neo.Benchmarks/Neo.Benchmarks.csproj @@ -10,9 +10,12 @@ + + + + - diff --git a/src/Neo.Extensions/ByteArrayEqualityComparer.cs b/src/Neo.Extensions/ByteArrayEqualityComparer.cs index 50861083b2..269e07f616 100644 --- a/src/Neo.Extensions/ByteArrayEqualityComparer.cs +++ b/src/Neo.Extensions/ByteArrayEqualityComparer.cs @@ -21,7 +21,7 @@ public class ByteArrayEqualityComparer : IEqualityComparer public bool Equals(byte[]? x, byte[]? y) { if (ReferenceEquals(x, y)) return true; - if (x is null || y is null || x.Length != y.Length) return false; + if (x is null || y is null) return false; return x.AsSpan().SequenceEqual(y.AsSpan()); } diff --git a/src/Neo/Collections/Caching/StoreCache.cs b/src/Neo/Collections/Caching/StoreCache.cs index c86e74b6e4..36fe11c5f4 100644 --- a/src/Neo/Collections/Caching/StoreCache.cs +++ b/src/Neo/Collections/Caching/StoreCache.cs @@ -41,7 +41,8 @@ public TValue this[TKey key] } set { - TryAddSync(key, value); + if (TryUpdateSync(key, value) == false) + AddSync(key, value); } } @@ -57,7 +58,8 @@ public object this[object key] } set { - TryAddSync(key as TKey, value as TValue); + if (TryUpdateSync(key as TKey, value as TValue) == false) + AddSync(key as TKey, value as TValue); } } @@ -71,7 +73,7 @@ public object this[object key] public bool IsFixedSize => false; /// - public bool IsSynchronized => false; + public bool IsSynchronized => true; /// public object SyncRoot => throw new NotSupportedException(); @@ -97,20 +99,20 @@ public object this[object key] /// public void Add(TKey key, TValue value) { - TryAddSync(key, value); + AddSync(key, value); } /// public void Add(KeyValuePair item) { - TryAddSync(item.Key, item.Value); + AddSync(item.Key, item.Value); } /// public void Add(object key, object value) { if (key is TKey tKey && value is TValue tValue) - TryAddSync(tKey, tValue); + AddSync(tKey, tValue); } /// @@ -122,6 +124,7 @@ public void Clear() /// public bool Contains(KeyValuePair item) { + // Doesn't have to be in both return s_memoryCache.ContainsKey(item.Key) || _store.Contains(item.Key.ToArray()); } @@ -129,13 +132,16 @@ public bool Contains(KeyValuePair item) public bool Contains(object key) { if (key is TKey tKey) + // Doesn't have to be in both return s_memoryCache.ContainsKey(tKey) || _store.Contains(tKey.ToArray()); ; + return false; } /// public bool ContainsKey(TKey key) { + // Doesn't have to be in both return s_memoryCache.ContainsKey(key) || _store.Contains(key.ToArray()); } @@ -154,20 +160,20 @@ public void CopyTo(Array array, int index) /// public bool Remove(TKey key) { - return TryRemoveSync(key, out _); + return TryRemoveSync(key); } /// public bool Remove(KeyValuePair item) { - return TryRemoveSync(item.Key, out _); + return TryRemoveSync(item.Key); } /// public void Remove(object key) { if (key is TKey tKey) - TryRemoveSync(tKey, out _); + TryRemoveSync(tKey); } /// @@ -176,6 +182,22 @@ public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue value) return TryGetSync(key, out value); } + public bool Update(TKey key, TValue value) + { + return TryUpdateSync(key, value); + } + + public bool Update(KeyValuePair item) + { + return TryUpdateSync(item.Key, item.Value); + } + + public void Update(object key, object value) + { + if (key is TKey tKey && value is TValue tValue) + TryUpdateSync(tKey, tValue); + } + /// public IEnumerator> GetEnumerator() { @@ -202,28 +224,15 @@ private static TValue GetTargetValue(WeakReference valueRef) throw new NullReferenceException(); } - private bool TryRemoveSync(TKey key, [NotNullWhen(true)] out TValue value) + private bool TryRemoveSync(TKey key) { - if (s_memoryCache.TryRemove(key, out var valueRef)) + if (s_memoryCache.TryRemove(key, out _)) { _store.Delete(key.ToArray()); - - return valueRef.TryGetTarget(out value); + return true; } - else - { - if (_store.TryGet(key.ToArray(), out var rawValue)) - { - value = rawValue.AsSerializable(); - - _store.Delete(key.ToArray()); - - return true; - } - value = default; - return false; - } + return false; } private bool TryGetSync(TKey key, [NotNullWhen(true)] out TValue value) @@ -257,42 +266,41 @@ private bool TryGetSync(TKey key, [NotNullWhen(true)] out TValue value) return false; } - private bool TryAddSync(TKey key, TValue value) + private void AddSync(TKey key, TValue value) + { + if (s_memoryCache.ContainsKey(key)) + return; + + if (s_memoryCache.TryAdd(key, new(value, false))) + // NOTE: + // This method of sync can change the + // "value" serializable type. If two + // caching classes use the same key + // but different ISerializable classes + _store.Put(key.ToArray(), value.ToArray()); + } + + private bool TryUpdateSync(TKey key, TValue value) { if (s_memoryCache.TryGetValue(key, out var valueRef)) { - if (valueRef.TryGetTarget(out var oldValue) && ReferenceEquals(value, oldValue) == false) - { - // Update target for cache + if (valueRef.TryGetTarget(out var oldValue) == false) valueRef.SetTarget(value); - - // Save to store - // - // NOTE: - // This method of sync can change the - // "value" serializable type. If two - // caching classes use the same key - // but different ISerializable classes - _store.Put(key.ToArray(), value.ToArray()); + else + { + // `value` isn't the same instance + if (ReferenceEquals(value, oldValue) == false) + valueRef.SetTarget(value); } - return true; - } - else - { - if (s_memoryCache.TryAdd(key, new(value, false))) - { - // Save to store - // - // NOTE: - // This method of sync can change the - // "value" serializable type. If two - // caching classes use the same key - // but different ISerializable classes - _store.Put(key.ToArray(), value.ToArray()); + // NOTE: + // This method of sync can change the + // "value" serializable type. If two + // caching classes use the same key + // but different ISerializable classes + _store.Put(key.ToArray(), value.ToArray()); - return true; - } + return true; } return false; diff --git a/src/Neo/Persistence/MemoryStore.cs b/src/Neo/Persistence/MemoryStore.cs index 05a08e30d9..7d6f2f4f35 100644 --- a/src/Neo/Persistence/MemoryStore.cs +++ b/src/Neo/Persistence/MemoryStore.cs @@ -12,6 +12,7 @@ #nullable enable using Neo.Extensions; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -33,7 +34,10 @@ public void Delete(byte[] key) _innerData.TryRemove(key, out _); } - public void Dispose() { } + public void Dispose() + { + GC.SuppressFinalize(this); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public ISnapshot GetSnapshot() @@ -44,7 +48,7 @@ public ISnapshot GetSnapshot() [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Put(byte[] key, byte[] value) { - _innerData[key[..]] = value[..]; + _innerData[key] = value; } /// @@ -59,14 +63,14 @@ public void Put(byte[] key, byte[] value) records = records.Where(p => comparer.Compare(p.Key, keyOrPrefix) >= 0); records = records.OrderBy(p => p.Key, comparer); foreach (var pair in records) - yield return (pair.Key[..], pair.Value[..]); + yield return (pair.Key, pair.Value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte[]? TryGet(byte[] key) { if (!_innerData.TryGetValue(key, out var value)) return null; - return value[..]; + return value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] From b99d252dfb905cc30c001a85bb9c1c5d2a6b0a18 Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt Date: Thu, 30 Jan 2025 18:33:52 -0500 Subject: [PATCH 4/7] Added `StorageCache` --- .../IO/Caching/Benchmarks.StorageCache.cs | 104 ++++++++ benchmarks/Neo.Benchmarks/Program.cs | 4 +- src/Neo/Collections/Caching/StoreCache.cs | 2 +- .../KeyValueSerializableEqualityComparer.cs | 21 +- src/Neo/IO/Caching/CacheEvictionReason.cs | 49 ++++ src/Neo/IO/Caching/IStorageCache.cs | 44 ++++ src/Neo/IO/Caching/IStorageEntry.cs | 44 ++++ src/Neo/IO/Caching/StorageCache.cs | 248 ++++++++++++++++++ src/Neo/IO/Caching/StorageEntry.cs | 167 ++++++++++++ .../IO/Caching/UT_StorageCache.cs | 109 ++++++++ 10 files changed, 783 insertions(+), 9 deletions(-) create mode 100644 benchmarks/Neo.Benchmarks/IO/Caching/Benchmarks.StorageCache.cs create mode 100644 src/Neo/IO/Caching/CacheEvictionReason.cs create mode 100644 src/Neo/IO/Caching/IStorageCache.cs create mode 100644 src/Neo/IO/Caching/IStorageEntry.cs create mode 100644 src/Neo/IO/Caching/StorageCache.cs create mode 100644 src/Neo/IO/Caching/StorageEntry.cs create mode 100644 tests/Neo.UnitTests/IO/Caching/UT_StorageCache.cs diff --git a/benchmarks/Neo.Benchmarks/IO/Caching/Benchmarks.StorageCache.cs b/benchmarks/Neo.Benchmarks/IO/Caching/Benchmarks.StorageCache.cs new file mode 100644 index 0000000000..e850dd5ad8 --- /dev/null +++ b/benchmarks/Neo.Benchmarks/IO/Caching/Benchmarks.StorageCache.cs @@ -0,0 +1,104 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Benchmarks.StorageCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Order; +using Neo.IO.Caching; +using Neo.Persistence; +using Neo.SmartContract; +using Perfolizer.Mathematics.OutlierDetection; +using System.Diagnostics; + +namespace Neo.Benchmark.IO.Caching +{ + // Result Exporters + [MarkdownExporter] // Exporting results in Markdown format + // Result Output + [MinColumn, MaxColumn, MeanColumn, MedianColumn] // Include these columns + [Orderer(SummaryOrderPolicy.Declared, MethodOrderPolicy.Declared)] // Keep in current order as declared in class + // Job Configurations + [SimpleJob(RuntimeMoniker.Net90)] + [Outliers(OutlierMode.DontRemove)] + [GcServer(true)] // GC server is enabled for GitHub builds in `neo` repo + [GcConcurrent(true)] // GC runs on it own thread + [GcForce(false)] // DO NOT force full collection of data for each benchmark + public class Benchmarks_StorageCache + { + private readonly MemoryStore _memoryStore = new(); + private readonly StorageCache _storageCache; + private readonly DataCache _dataCache; + + private readonly StorageKey _key; + private readonly StorageItem _value; + + public Benchmarks_StorageCache() + { + _storageCache = new(_memoryStore); + _dataCache = new SnapshotCache(_memoryStore); + + var data = new byte[1024]; + new Random(0xdead).NextBytes(data); + + _memoryStore.Put(data, data); + _key = new(data); + _value = new(data); + } + + [Benchmark] + public void TestStoreCacheAdd() + { + _storageCache.AddOrUpdate(_key, _value); + } + + [Benchmark] + public void TestDataCacheAdd() + { + Debug.Assert(_dataCache.GetOrAdd(_key, () => _value) != null); + } + + [Benchmark] + public void TestStoreCacheUpdate() + { + _storageCache.AddOrUpdate(_key, _value); + } + + [Benchmark] + public void TestDataCacheUpdate() + { + Debug.Assert(_dataCache.GetAndChange(_key, () => _value) != null); + } + + [Benchmark] + public void TestStoreCacheDelete() + { + _storageCache.Delete(_key); + } + + [Benchmark] + public void TestDataCacheDelete() + { + _dataCache.Delete(_key); + } + + [Benchmark] + public void TestStoreCacheGet() + { + Debug.Assert(_storageCache.TryGetValue(_key, out _)); + } + + [Benchmark] + public void TestDataCacheGet() + { + Debug.Assert(_dataCache.GetAndChange(_key) != null); + } + } +} diff --git a/benchmarks/Neo.Benchmarks/Program.cs b/benchmarks/Neo.Benchmarks/Program.cs index 96c919d313..d580d4907e 100644 --- a/benchmarks/Neo.Benchmarks/Program.cs +++ b/benchmarks/Neo.Benchmarks/Program.cs @@ -10,7 +10,7 @@ // modifications are permitted. using BenchmarkDotNet.Running; -using Neo.Benchmark.Collections.Caching; +using Neo.Benchmark.IO.Caching; // BenchmarkRunner.Run(); //BenchmarkRunner.Run(); @@ -18,4 +18,4 @@ //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); -BenchmarkRunner.Run(); +BenchmarkRunner.Run(); diff --git a/src/Neo/Collections/Caching/StoreCache.cs b/src/Neo/Collections/Caching/StoreCache.cs index 36fe11c5f4..87ec8e14f7 100644 --- a/src/Neo/Collections/Caching/StoreCache.cs +++ b/src/Neo/Collections/Caching/StoreCache.cs @@ -25,7 +25,7 @@ public class StoreCache(IStore store) : ICollection> s_memoryCache = new(Math.Min(Environment.ProcessorCount, 16), 0, KeyValueSerializableEqualityComparer.Default); + private static readonly ConcurrentDictionary> s_memoryCache = new(Math.Min(Environment.ProcessorCount, 16), 0, KeyValueSerializableEqualityComparer.Instance); private readonly IStore _store = store ?? throw new ArgumentNullException(nameof(store)); diff --git a/src/Neo/Collections/KeyValueSerializableEqualityComparer.cs b/src/Neo/Collections/KeyValueSerializableEqualityComparer.cs index 355b7ddc6c..f279eb47d3 100644 --- a/src/Neo/Collections/KeyValueSerializableEqualityComparer.cs +++ b/src/Neo/Collections/KeyValueSerializableEqualityComparer.cs @@ -11,15 +11,16 @@ using Neo.IO; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; namespace Neo.Extensions { - internal class KeyValueSerializableEqualityComparer : IEqualityComparer - where TKey : IKeySerializable + internal class KeyValueSerializableEqualityComparer : IEqualityComparer, IEqualityComparer + where TKey : class, IKeySerializable { - public static readonly KeyValueSerializableEqualityComparer Default = new(); + public static readonly KeyValueSerializableEqualityComparer Instance = new(); public bool Equals(TKey x, TKey y) { @@ -29,11 +30,19 @@ public bool Equals(TKey x, TKey y) return x.ToArray().AsSpan().SequenceEqual(y.ToArray().AsSpan()); } + public new bool Equals(object x, object y) + { + return Equals(x as TKey, y as TKey); + } + public int GetHashCode(TKey obj) { - if (obj is null) - throw new ArgumentNullException(nameof(obj)); - return obj.GetHashCode(); + return obj is null ? 0 : obj.GetHashCode(); + } + + public int GetHashCode(object obj) + { + return obj is TKey t ? GetHashCode(t) : 0; } } } diff --git a/src/Neo/IO/Caching/CacheEvictionReason.cs b/src/Neo/IO/Caching/CacheEvictionReason.cs new file mode 100644 index 0000000000..2f61ee3246 --- /dev/null +++ b/src/Neo/IO/Caching/CacheEvictionReason.cs @@ -0,0 +1,49 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// CacheEvictionReason.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.IO.Caching +{ + /// + /// Specifies the reasons why an entry was evicted from the cache. + /// + public enum CacheEvictionReason : byte + { + /// + /// The item was not removed from the cache. + /// + None = 0, + + /// + /// The item was removed from the cache manually. + /// + Removed = 1, + + /// + /// The item was removed from the cache because it was overwritten. + /// + Replaced = 2, + + /// + /// The item was removed from the cache because it timed out. + /// + Expired = 3, + + /// + /// The item was removed from the cache because its token expired. + /// + TokenExpired = 4, + + /// + /// The item was removed from the cache because it exceeded its capacity. + /// + Capacity = 5, + } +} diff --git a/src/Neo/IO/Caching/IStorageCache.cs b/src/Neo/IO/Caching/IStorageCache.cs new file mode 100644 index 0000000000..1a818a42cf --- /dev/null +++ b/src/Neo/IO/Caching/IStorageCache.cs @@ -0,0 +1,44 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IStorageCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; + +namespace Neo.IO.Caching +{ + /// + /// Represents a local storage cache whose values are not serialized. + /// + internal interface IStorageCache : IDisposable + where TKey : class, IKeySerializable + where TValue : class, ISerializable, new() + { + /// + /// Gets the item associated with this key if present. + /// + /// An object identifying the requested entry. + /// The located value or null. + /// if the key was found. + bool TryGetValue(TKey key, out TValue value); + + /// + /// Create an entry in the cache. + /// + /// An object identifying the entry. + /// The object you want to cache. + void AddOrUpdate(TKey key, TValue value); + + /// + /// Removes the object associated with the given key. + /// + /// An object identifying the entry. + void Remove(TKey key); + } +} diff --git a/src/Neo/IO/Caching/IStorageEntry.cs b/src/Neo/IO/Caching/IStorageEntry.cs new file mode 100644 index 0000000000..602dd576f9 --- /dev/null +++ b/src/Neo/IO/Caching/IStorageEntry.cs @@ -0,0 +1,44 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// IStorageEntry.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; + +namespace Neo.IO.Caching +{ + /// + /// Represents an entry in the implementation. + /// When Disposed, is committed to the cache. + /// + internal interface IStorageEntry : IDisposable + where TKey : class, IKeySerializable + where TValue : class, ISerializable, new() + { + /// + /// Gets the key of the storage entry. + /// + TKey Key { get; } + + /// + /// Gets or set the value of the storage entry. + /// + TValue Value { get; set; } + + /// + /// Gets or sets an absolute expiration date for the cache entry. + /// + DateTimeOffset? AbsoluteExpiration { get; set; } + + /// + /// Gets or sets an absolute expiration time, relative to now. + /// + TimeSpan? AbsoluteExpirationRelativeToNow { get; set; } + } +} diff --git a/src/Neo/IO/Caching/StorageCache.cs b/src/Neo/IO/Caching/StorageCache.cs new file mode 100644 index 0000000000..940d4488b2 --- /dev/null +++ b/src/Neo/IO/Caching/StorageCache.cs @@ -0,0 +1,248 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StorageCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.Persistence; +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo.IO.Caching +{ + /// + /// Implements using a dictionary to + /// store its entries. + /// + internal class StorageCache(IStore store) : IStorageCache + where TKey : class, IKeySerializable + where TValue : class, ISerializable, new() + { + public int Size => _cacheEntries.Count; + + private static readonly TimeSpan s_scanTimeIntervals = TimeSpan.FromMinutes(1); + + public static DateTime UtcNow => DateTime.UtcNow; + + private readonly ConcurrentDictionary> _cacheEntries = new(KeyValueSerializableEqualityComparer.Instance); + private readonly IStore _store = store ?? throw new ArgumentNullException(nameof(store)); + + private DateTime _lastExpirationScan; + private bool _disposed; + + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Disposes the cache and clears all entries. + /// + /// to dispose the object resources; to take no action. + protected virtual void Dispose(bool disposing) + { + if (_disposed == false) + { + if (disposing) + { + _cacheEntries.Clear(); + GC.SuppressFinalize(this); + } + + _disposed = true; + } + } + + /// + public void AddOrUpdate(TKey key, TValue value) + { + ValidateCacheKey(key); + CheckDisposed(); + + var utcNow = UtcNow; + + if (_cacheEntries.TryGetValue(key, out var tmp)) + { + var entry = tmp; + + entry.LastAccessed = utcNow; + entry.Value = value; + entry.SetExpirationTimeRelativeTo(utcNow); + + _store.Put(key.ToArray(), value.ToArray()); + } + else + { + var entry = new StorageEntry(key, this) + { + Value = value, + LastAccessed = utcNow, + }; + + entry.SetExpirationTimeRelativeTo(utcNow); + + if (_cacheEntries.TryAdd(key, entry)) + _store.Put(key.ToArray(), value.ToArray()); + } + + StartScanForExpiredItemsIfNeeded(utcNow); + } + + /// + public void Remove(TKey key) + { + ValidateCacheKey(key); + CheckDisposed(); + + if (_cacheEntries.TryRemove(key, out var entry)) + entry.SetExpired(CacheEvictionReason.Removed); + + StartScanForExpiredItemsIfNeeded(UtcNow); + } + + public void Delete(TKey key) + { + ValidateCacheKey(key); + CheckDisposed(); + + if (_cacheEntries.TryRemove(key, out var entry)) + { + entry.SetExpired(CacheEvictionReason.Removed); + _store.Delete(key.ToArray()); + } + + StartScanForExpiredItemsIfNeeded(UtcNow); + } + + /// + public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue value) + { + ValidateCacheKey(key); + CheckDisposed(); + + var utcNow = UtcNow; + + if (_cacheEntries.TryGetValue(key, out var tmp)) + { + var entry = tmp; + + if (entry.CheckExpired(utcNow)) + { + if (_store.TryGet(key.ToArray(), out var rawEntryValue) == false) + _cacheEntries.TryRemove(entry.Key, out _); + else + { + entry.LastAccessed = utcNow; + value = rawEntryValue.AsSerializable(); + entry.Value = value; + + entry.SetExpirationTimeRelativeTo(utcNow); + StartScanForExpiredItemsIfNeeded(utcNow); + + return true; + } + } + else + { + entry.LastAccessed = utcNow; + value = entry.Value; + + entry.SetExpirationTimeRelativeTo(utcNow); + StartScanForExpiredItemsIfNeeded(utcNow); + + return true; + } + } + else + { + if (_store.TryGet(key.ToArray(), out var rawEntryValue)) + { + var entry = new StorageEntry(key, this) + { + Value = rawEntryValue.AsSerializable(), + LastAccessed = utcNow, + }; + + entry.SetExpirationTimeRelativeTo(utcNow); + + if (_cacheEntries.TryAdd(key, entry)) + { + value = entry.Value; + return true; + } + } + } + + StartScanForExpiredItemsIfNeeded(utcNow); + + value = default; + return false; + } + + /// + /// Removes all keys and values from the cache. + /// + public void Clear() + { + CheckDisposed(); + + foreach (var entry in _cacheEntries.Values) + { + entry.SetExpired(CacheEvictionReason.Removed); + + if (_cacheEntries.TryRemove(entry.Key, out _)) + StartScanForExpiredItemsIfNeeded(UtcNow); + } + } + + private static void ValidateCacheKey(TKey key) + { + _ = key ?? throw new ArgumentNullException(nameof(key)); + } + + private void CheckDisposed() + { + if (_disposed) + Throw(); + + [DoesNotReturn] + static void Throw() => throw new ObjectDisposedException(typeof(StorageCache).FullName); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void StartScanForExpiredItemsIfNeeded(DateTime utcNow) + { + if (utcNow - _lastExpirationScan >= s_scanTimeIntervals) + ScheduleTask(utcNow); + + void ScheduleTask(DateTime utcNow) + { + _lastExpirationScan = utcNow; + Task.Factory.StartNew(state => ((StorageCache)state!).ScanForExpiredItems(), this, + CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } + } + + private void ScanForExpiredItems() + { + var utcNow = _lastExpirationScan = UtcNow; + + foreach (var entry in _cacheEntries.Values) + { + if (entry.CheckExpired(utcNow)) + _cacheEntries.TryRemove(entry.Key, out _); + } + } + } +} diff --git a/src/Neo/IO/Caching/StorageEntry.cs b/src/Neo/IO/Caching/StorageEntry.cs new file mode 100644 index 0000000000..570ece95da --- /dev/null +++ b/src/Neo/IO/Caching/StorageEntry.cs @@ -0,0 +1,167 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StorageEntry.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Runtime.CompilerServices; + +namespace Neo.IO.Caching +{ + internal class StorageEntry( + TKey key, + StorageCache cache, + TimeSpan? ttl = null) : IStorageEntry + where TKey : class, IKeySerializable + where TValue : class, ISerializable, new() + { + private const int NotSet = -1; + + internal static readonly TimeSpan DefaultTimeToLive = TimeSpan.FromSeconds(30); + + public TKey Key { get; } = key ?? throw new ArgumentNullException(nameof(key)); + + public TValue Value + { + get => _value; + set => _value = value; + } + + TimeSpan? IStorageEntry.AbsoluteExpirationRelativeToNow + { + get => _absoluteExpirationRelativeToNow.Ticks == 0 ? null : _absoluteExpirationRelativeToNow; + set + { + if (value is { Ticks: <= 0 }) + throw new ArgumentOutOfRangeException(nameof(AbsoluteExpirationRelativeToNow), value, "The relative expiration value must be positive."); + + _absoluteExpirationRelativeToNow = value.GetValueOrDefault(); + } + } + + DateTimeOffset? IStorageEntry.AbsoluteExpiration + { + get + { + if (_absoluteExpirationTicks < 0) + return null; + + var offset = new TimeSpan(_absoluteExpirationOffsetMinutes * TimeSpan.TicksPerMinute); + return new DateTimeOffset(_absoluteExpirationTicks + offset.Ticks, offset); + } + set + { + if (value is null) + { + _absoluteExpirationTicks = NotSet; + _absoluteExpirationOffsetMinutes = default; + } + else + { + var expiration = value.GetValueOrDefault(); + _absoluteExpirationTicks = expiration.UtcTicks; + _absoluteExpirationOffsetMinutes = (short)(expiration.Offset.Ticks / TimeSpan.TicksPerMinute); + } + } + } + + internal long AbsoluteExpirationTicks + { + get => _absoluteExpirationTicks; + set + { + _absoluteExpirationTicks = value; + _absoluteExpirationOffsetMinutes = 0; + } + } + + internal DateTime LastAccessed { get; set; } + internal TimeSpan AbsoluteExpirationRelativeToNow => _absoluteExpirationRelativeToNow; + + internal CacheEvictionReason EvictionReason + { + get => _evictionReason; + private set => _evictionReason = value; + } + + private readonly StorageCache _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + + private TValue _value; + + private CacheEvictionReason _evictionReason; + private TimeSpan _absoluteExpirationRelativeToNow = ttl ?? DefaultTimeToLive; + private long _absoluteExpirationTicks = NotSet; + private short _absoluteExpirationOffsetMinutes; + + private bool _isDisposed; + private bool _isExpired; + + /// + public void Dispose() + { + if (_isDisposed == false) + { + _isDisposed = true; + _cache.Remove(Key); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling + internal bool CheckExpired(DateTime utcNow) + => _isExpired || + CheckForExpiredTime(utcNow); + + internal void SetExpired(CacheEvictionReason reason) + { + if (EvictionReason == CacheEvictionReason.None) + { + EvictionReason = reason; + } + _isExpired = true; + } + + internal void SetTimeout(TimeSpan? expires) + { + if (expires is { Ticks: <= 0 }) + throw new ArgumentOutOfRangeException(nameof(expires), expires, "The relative expiration value must be positive."); + _absoluteExpirationRelativeToNow = expires.GetValueOrDefault(); + } + + internal void SetExpirationTimeRelativeTo(DateTime utcNow) + { + if (_absoluteExpirationRelativeToNow.Ticks > 0) + { + var absoluteExpiration = (utcNow + _absoluteExpirationRelativeToNow).Ticks; + + if ((ulong)absoluteExpiration < (ulong)_absoluteExpirationTicks) + _absoluteExpirationTicks = absoluteExpiration; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling + private bool CheckForExpiredTime(DateTime utcNow) + { + if (_absoluteExpirationTicks < 0) + return false; + + return FullCheck(utcNow); + + bool FullCheck(DateTime utcNow) + { + if ((ulong)_absoluteExpirationTicks <= (ulong)utcNow.Ticks) + { + SetExpired(CacheEvictionReason.Expired); + return true; + } + + return false; + } + } + } +} diff --git a/tests/Neo.UnitTests/IO/Caching/UT_StorageCache.cs b/tests/Neo.UnitTests/IO/Caching/UT_StorageCache.cs new file mode 100644 index 0000000000..b5bc680c97 --- /dev/null +++ b/tests/Neo.UnitTests/IO/Caching/UT_StorageCache.cs @@ -0,0 +1,109 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_StorageCache.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Extensions; +using Neo.IO.Caching; +using Neo.Persistence; +using Neo.SmartContract; +using System.Linq; + +namespace Neo.UnitTests.IO.Caching +{ + [TestClass] + public class UT_StorageCache + { + [TestMethod] + public void TestStorageCacheAddAndUpdate() + { + using var memoryStore = new MemoryStore(); + using var storeCache = new StorageCache(memoryStore); + + var expectedKey = new StorageKey([0, 1, 2, 3, 4, 5, 6]); + var expectedAddValue = new StorageItem([7, 8, 9, 0, 11, 12]); + var expectedUpdateValue = new StorageItem([13, 14, 15, 16, 17, 18]); + + storeCache.AddOrUpdate(expectedKey, expectedAddValue); + + var actualStoreAddValue = memoryStore.TryGet(expectedKey.ToArray()); + var actualCacheAddValueRet = storeCache.TryGetValue(expectedKey, out var actualCacheAddValue); + + Assert.IsNotNull(actualStoreAddValue); + CollectionAssert.AreEqual(expectedAddValue.ToArray(), actualStoreAddValue); + + Assert.IsTrue(actualCacheAddValueRet); + Assert.IsNotNull(actualCacheAddValue); + Assert.AreSame(expectedAddValue, actualCacheAddValue); + + storeCache.AddOrUpdate(expectedKey, expectedUpdateValue); + + var actualStoreUpdateValue = memoryStore.TryGet(expectedKey.ToArray()); + var actualCacheUpdateValueRet = storeCache.TryGetValue(expectedKey, out var actualCacheUpdateValue); + + Assert.IsNotNull(actualStoreUpdateValue); + CollectionAssert.AreEqual(expectedUpdateValue.ToArray(), actualStoreUpdateValue); + + Assert.IsTrue(actualCacheUpdateValueRet); + Assert.IsNotNull(actualCacheUpdateValue); + Assert.AreSame(expectedUpdateValue, actualCacheUpdateValue); + } + + [TestMethod] + public void TestStorageCacheRemove() + { + using var memoryStore = new MemoryStore(); + using var storeCache = new StorageCache(memoryStore); + + var expectedKey = new StorageKey([0, 1, 2, 3, 4, 5, 6]); + var expectedAddValue = new StorageItem([7, 8, 9, 0, 11, 12]); + + storeCache.AddOrUpdate(expectedKey, expectedAddValue); + storeCache.Remove(expectedKey); + + Assert.AreEqual(0, storeCache.Size); + + var actualStoreAddValue = memoryStore.TryGet(expectedKey.ToArray()); + var actualCacheAddValueRet = storeCache.TryGetValue(expectedKey, out var actualCacheAddValue); + + Assert.IsNotNull(actualStoreAddValue); + CollectionAssert.AreEqual(expectedAddValue.ToArray(), actualStoreAddValue); + + Assert.IsTrue(actualCacheAddValueRet); + Assert.IsNotNull(actualCacheAddValue); + // NOTE: that when you remove from cache. `StorageCache` class has to get fetch from + // `IStore`. Making the instance of `TValue` different. + CollectionAssert.AreEqual(expectedAddValue.ToArray(), actualCacheAddValue.ToArray()); + } + + [TestMethod] + public void TestStorageCacheDelete() + { + using var memoryStore = new MemoryStore(); + using var storeCache = new StorageCache(memoryStore); + + var expectedKey = new StorageKey([0, 1, 2, 3, 4, 5, 6]); + var expectedAddValue = new StorageItem([7, 8, 9, 0, 11, 12]); + + storeCache.AddOrUpdate(expectedKey, expectedAddValue); + storeCache.Delete(expectedKey); + + Assert.AreEqual(0, storeCache.Size); + + var actualStoreAddValue = memoryStore.TryGet(expectedKey.ToArray()); + var actualCacheAddValueRet = storeCache.TryGetValue(expectedKey, out var actualCacheAddValue); + + Assert.IsNull(actualStoreAddValue); + + Assert.IsFalse(actualCacheAddValueRet); + Assert.IsNull(actualCacheAddValue); + } + } +} From 10a69d87bdd56470f55b93594d6494ecfe2f7bff Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt Date: Thu, 30 Jan 2025 21:46:03 -0500 Subject: [PATCH 5/7] typo --- tests/Neo.UnitTests/IO/Caching/UT_StorageCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Neo.UnitTests/IO/Caching/UT_StorageCache.cs b/tests/Neo.UnitTests/IO/Caching/UT_StorageCache.cs index b5bc680c97..c752414c6e 100644 --- a/tests/Neo.UnitTests/IO/Caching/UT_StorageCache.cs +++ b/tests/Neo.UnitTests/IO/Caching/UT_StorageCache.cs @@ -78,7 +78,7 @@ public void TestStorageCacheRemove() Assert.IsTrue(actualCacheAddValueRet); Assert.IsNotNull(actualCacheAddValue); - // NOTE: that when you remove from cache. `StorageCache` class has to get fetch from + // NOTE: that when you remove from cache. `StorageCache` class has to fetch from // `IStore`. Making the instance of `TValue` different. CollectionAssert.AreEqual(expectedAddValue.ToArray(), actualCacheAddValue.ToArray()); } From 51572d8d48fa15791d8cdf9be08641e5984077da Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt Date: Thu, 30 Jan 2025 23:38:57 -0500 Subject: [PATCH 6/7] Bug fixes and optimizations --- src/Neo.Extensions/ByteArrayComparer.cs | 22 +++++++++++-------- .../ByteArrayEqualityComparer.cs | 19 +++++++++++----- .../KeyValueSerializableEqualityComparer.cs | 12 +++++----- src/Neo/IO/Caching/StorageCache.cs | 4 +++- src/Neo/Persistence/MemorySnapshot.cs | 13 ++++++++--- src/Neo/Persistence/MemoryStore.cs | 12 ++++++---- tests/Neo.Plugins.Storage.Tests/StoreTest.cs | 20 +++++++++++++++++ .../Persistence/UT_MemoryStore.cs | 5 ----- 8 files changed, 74 insertions(+), 33 deletions(-) diff --git a/src/Neo.Extensions/ByteArrayComparer.cs b/src/Neo.Extensions/ByteArrayComparer.cs index 1a754622cb..53d88706fb 100644 --- a/src/Neo.Extensions/ByteArrayComparer.cs +++ b/src/Neo.Extensions/ByteArrayComparer.cs @@ -10,37 +10,41 @@ // modifications are permitted. using System; +using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; namespace Neo.Extensions { - public class ByteArrayComparer : IComparer + public class ByteArrayComparer : IComparer, IComparer { public static readonly ByteArrayComparer Default = new(1); public static readonly ByteArrayComparer Reverse = new(-1); - private readonly int _direction; + private readonly sbyte _direction; private ByteArrayComparer(int direction) { - _direction = direction; + _direction = unchecked((sbyte)direction); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Compare(byte[]? x, byte[]? y) { - if (x == y) return 0; + if (ReferenceEquals(x, y)) return 0; - if (x is null) // y must not be null - return -y!.Length * _direction; - - if (y is null) // x must not be null - return x.Length * _direction; + x ??= []; + y ??= []; if (_direction < 0) return y.AsSpan().SequenceCompareTo(x.AsSpan()); return x.AsSpan().SequenceCompareTo(y.AsSpan()); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Compare(object? x, object? y) + { + return Compare(x as byte[], y as byte[]); + } } } diff --git a/src/Neo.Extensions/ByteArrayEqualityComparer.cs b/src/Neo.Extensions/ByteArrayEqualityComparer.cs index 269e07f616..696af8903a 100644 --- a/src/Neo.Extensions/ByteArrayEqualityComparer.cs +++ b/src/Neo.Extensions/ByteArrayEqualityComparer.cs @@ -9,12 +9,13 @@ // Redistribution and use in source and binary forms with or without // modifications are permitted. -using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace Neo.Extensions { - public class ByteArrayEqualityComparer : IEqualityComparer + public class ByteArrayEqualityComparer : IEqualityComparer, IEqualityComparer { public static readonly ByteArrayEqualityComparer Default = new(); @@ -22,13 +23,21 @@ public bool Equals(byte[]? x, byte[]? y) { if (ReferenceEquals(x, y)) return true; if (x is null || y is null) return false; + if (x.Length != y.Length) return false; - return x.AsSpan().SequenceEqual(y.AsSpan()); + return GetHashCode(x) == GetHashCode(y); } - public int GetHashCode(byte[] obj) + public new bool Equals(object? x, object? y) { - return obj.XxHash3_32(); + if (ReferenceEquals(x, y)) return true; + return Equals(x as byte[], y as byte[]); } + + public int GetHashCode([DisallowNull] byte[] obj) => + obj.XxHash3_32(); + + public int GetHashCode([DisallowNull] object obj) => + obj is byte[] b ? GetHashCode(b) : 0; } } diff --git a/src/Neo/Collections/KeyValueSerializableEqualityComparer.cs b/src/Neo/Collections/KeyValueSerializableEqualityComparer.cs index f279eb47d3..f65374419f 100644 --- a/src/Neo/Collections/KeyValueSerializableEqualityComparer.cs +++ b/src/Neo/Collections/KeyValueSerializableEqualityComparer.cs @@ -10,10 +10,9 @@ // modifications are permitted. using Neo.IO; -using System; using System.Collections; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics.CodeAnalysis; namespace Neo.Extensions { @@ -25,9 +24,10 @@ internal class KeyValueSerializableEqualityComparer : IEqualityComparer public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward) { + keyOrPrefix ??= []; + + if (direction == SeekDirection.Backward && keyOrPrefix.Length == 0) yield break; var comparer = direction == SeekDirection.Forward ? ByteArrayComparer.Default : ByteArrayComparer.Reverse; + IEnumerable> records = _immutableData; + if (keyOrPrefix?.Length > 0) records = records.Where(p => comparer.Compare(p.Key, keyOrPrefix) >= 0); records = records.OrderBy(p => p.Key, comparer); - return records.Select(p => (p.Key[..], p.Value[..])); + + foreach (var pair in records) + yield return new(pair.Key, pair.Value); } public byte[]? TryGet(byte[] key) { _immutableData.TryGetValue(key, out var value); - return value?[..]; + return value; } public bool TryGet(byte[] key, [NotNullWhen(true)] out byte[]? value) diff --git a/src/Neo/Persistence/MemoryStore.cs b/src/Neo/Persistence/MemoryStore.cs index 7d6f2f4f35..f31cc4d5ca 100644 --- a/src/Neo/Persistence/MemoryStore.cs +++ b/src/Neo/Persistence/MemoryStore.cs @@ -55,15 +55,19 @@ public void Put(byte[] key, byte[] value) public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[]? keyOrPrefix, SeekDirection direction = SeekDirection.Forward) { keyOrPrefix ??= []; - if (direction == SeekDirection.Backward && keyOrPrefix.Length == 0) yield break; + if (direction == SeekDirection.Backward && keyOrPrefix.Length == 0) yield break; var comparer = direction == SeekDirection.Forward ? ByteArrayComparer.Default : ByteArrayComparer.Reverse; + IEnumerable> records = _innerData; + if (keyOrPrefix.Length > 0) - records = records.Where(p => comparer.Compare(p.Key, keyOrPrefix) >= 0); - records = records.OrderBy(p => p.Key, comparer); + records = records.Where(w => comparer.Compare(w.Key, keyOrPrefix) >= 0); + + records = records.OrderBy(o => o.Key, comparer); + foreach (var pair in records) - yield return (pair.Key, pair.Value); + yield return new(pair.Key, pair.Value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/tests/Neo.Plugins.Storage.Tests/StoreTest.cs b/tests/Neo.Plugins.Storage.Tests/StoreTest.cs index 96c3c466b8..b0d027ceb9 100644 --- a/tests/Neo.Plugins.Storage.Tests/StoreTest.cs +++ b/tests/Neo.Plugins.Storage.Tests/StoreTest.cs @@ -317,6 +317,26 @@ private void TestStorage(IStore store) CollectionAssert.AreEqual(new byte[] { 0x00, 0x00, 0x00 }, entries[0].Key); CollectionAssert.AreEqual(new byte[] { 0x00, 0x00, 0x01 }, entries[1].Key); CollectionAssert.AreEqual(new byte[] { 0x00, 0x01, 0x02 }, entries[2].Key); + + // Seek null + entries = store.Seek(null, SeekDirection.Backward).ToArray(); + Assert.AreEqual(0, entries.Length); + + // Seek empty + entries = store.Seek([], SeekDirection.Backward).ToArray(); + Assert.AreEqual(0, entries.Length); + + // Test Snapshot + using (var snapshot = store.GetSnapshot()) + { + // Seek null + entries = snapshot.Seek(null, SeekDirection.Backward).ToArray(); + Assert.AreEqual(0, entries.Length); + + // Seek empty + entries = snapshot.Seek([], SeekDirection.Backward).ToArray(); + Assert.AreEqual(0, entries.Length); + } } } diff --git a/tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs b/tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs index c39d6cfd41..b73f627aef 100644 --- a/tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs +++ b/tests/Neo.UnitTests/Persistence/UT_MemoryStore.cs @@ -11,7 +11,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.Extensions; -using Neo.IO; using Neo.Persistence; using Neo.SmartContract; using System; @@ -133,10 +132,6 @@ public void NeoSystemStoreGetAndChange() storeView.Add(new KeyBuilder(1, 0x000002), new StorageItem([0x02])); storeView.Add(new KeyBuilder(1, 0x000003), new StorageItem([0x03])); storeView.Add(new KeyBuilder(1, 0x000004), new StorageItem([0x04])); - - var entries = storeView.Seek([], SeekDirection.Backward).ToArray(); - // Memory store has different seek behavior than the snapshot - Assert.AreEqual(entries.Length, 37); } } } From a724168e8a981033422a044b24bc4ff5547d078b Mon Sep 17 00:00:00 2001 From: Christopher Schuchardt Date: Fri, 31 Jan 2025 01:28:22 -0500 Subject: [PATCH 7/7] Fixed memory allocation problems --- src/Neo/Extensions/MemoryExtensions.cs | 10 ++++ src/Neo/SmartContract/StorageItem.cs | 83 +++++++++++++------------- src/Neo/SmartContract/StorageKey.cs | 30 +++++----- 3 files changed, 66 insertions(+), 57 deletions(-) diff --git a/src/Neo/Extensions/MemoryExtensions.cs b/src/Neo/Extensions/MemoryExtensions.cs index ef3d921a6f..1e1e476e54 100644 --- a/src/Neo/Extensions/MemoryExtensions.cs +++ b/src/Neo/Extensions/MemoryExtensions.cs @@ -70,5 +70,15 @@ public static int GetVarSize(this ReadOnlyMemory value) { return UnsafeData.GetVarSize(value.Length) + value.Length; } + + /// + /// Gets the size of the specified array encoded in variable-length encoding. + /// + /// The specified array. + /// The size of the array. + public static int GetVarSize(this Memory value) + { + return UnsafeData.GetVarSize(value.Length) + value.Length; + } } } diff --git a/src/Neo/SmartContract/StorageItem.cs b/src/Neo/SmartContract/StorageItem.cs index d76811b46f..ddf9c1411b 100644 --- a/src/Neo/SmartContract/StorageItem.cs +++ b/src/Neo/SmartContract/StorageItem.cs @@ -23,8 +23,8 @@ namespace Neo.SmartContract /// public class StorageItem : ISerializable { - private ReadOnlyMemory value; - private object cache; + private ReadOnlyMemory _value; + private object _cache; public int Size => Value.GetVarSize(); @@ -33,20 +33,17 @@ public class StorageItem : ISerializable /// public ReadOnlyMemory Value { - get + get => _value.IsEmpty == false ? _value : _value = _cache switch { - return !value.IsEmpty ? value : value = cache switch - { - BigInteger bi => bi.ToByteArrayStandard(), - IInteroperable interoperable => BinarySerializer.Serialize(interoperable.ToStackItem(null), ExecutionEngineLimits.Default), - null => ReadOnlyMemory.Empty, - _ => throw new InvalidCastException() - }; - } + BigInteger bi => bi.ToByteArrayStandard(), + IInteroperable interoperable => BinarySerializer.Serialize(interoperable.ToStackItem(null), ExecutionEngineLimits.Default), + null => ReadOnlyMemory.Empty, + _ => throw new InvalidCastException() + }; set { - this.value = value; - cache = null; + _value = value.ToArray(); // create new memory region + _cache = null; } } @@ -61,7 +58,7 @@ public StorageItem() { } /// The byte array value of the . public StorageItem(byte[] value) { - this.value = value; + _value = value[..].AsMemory(); // allocate new byte array with new memory space } /// @@ -70,7 +67,7 @@ public StorageItem(byte[] value) /// The integer value of the . public StorageItem(BigInteger value) { - cache = value; + _cache = value; } /// @@ -79,7 +76,7 @@ public StorageItem(BigInteger value) /// The value of the . public StorageItem(IInteroperable interoperable) { - cache = interoperable; + _cache = interoperable; } /// @@ -97,16 +94,18 @@ public void Add(BigInteger integer) /// The created . public StorageItem Clone() { - return new() + var newItem = new StorageItem { - value = value, - cache = cache is IInteroperable interoperable ? interoperable.Clone() : cache + _value = _value.ToArray(), // allocate new memory space + _cache = _cache is IInteroperable interoperable ? interoperable.Clone() : _cache, }; + + return newItem; } public void Deserialize(ref MemoryReader reader) { - Value = reader.ReadToEnd(); + Value = reader.ReadToEnd(); // allocate new memory space } /// @@ -115,17 +114,17 @@ public void Deserialize(ref MemoryReader reader) /// The instance to be copied. public void FromReplica(StorageItem replica) { - value = replica.value; - if (replica.cache is IInteroperable interoperable) + _value = replica._value.ToArray(); // allocate new memory space + if (replica._cache is IInteroperable interoperable) { - if (cache?.GetType() == interoperable.GetType()) - ((IInteroperable)cache).FromReplica(interoperable); + if (_cache?.GetType() == interoperable.GetType()) + ((IInteroperable)_cache).FromReplica(interoperable); else - cache = interoperable.Clone(); + _cache = interoperable.Clone(); } else { - cache = replica.cache; + _cache = replica._cache; } } @@ -136,14 +135,14 @@ public void FromReplica(StorageItem replica) /// The in the storage. public T GetInteroperable() where T : IInteroperable, new() { - if (cache is null) + if (_cache is null) { var interoperable = new T(); - interoperable.FromStackItem(BinarySerializer.Deserialize(value, ExecutionEngineLimits.Default)); - cache = interoperable; + interoperable.FromStackItem(BinarySerializer.Deserialize(_value, ExecutionEngineLimits.Default)); + _cache = interoperable; } - value = null; - return (T)cache; + _value = ReadOnlyMemory.Empty; // garbage collect the unallocated memory space + return (T)_cache; } /// @@ -154,14 +153,14 @@ public void FromReplica(StorageItem replica) /// The in the storage. public T GetInteroperable(bool verify = true) where T : IInteroperableVerifiable, new() { - if (cache is null) + if (_cache is null) { var interoperable = new T(); - interoperable.FromStackItem(BinarySerializer.Deserialize(value, ExecutionEngineLimits.Default), verify); - cache = interoperable; + interoperable.FromStackItem(BinarySerializer.Deserialize(_value, ExecutionEngineLimits.Default), verify); + _cache = interoperable; } - value = null; - return (T)cache; + _value = ReadOnlyMemory.Empty; // garbage collect the unallocated memory space + return (T)_cache; } public void Serialize(BinaryWriter writer) @@ -175,8 +174,8 @@ public void Serialize(BinaryWriter writer) /// The integer value to set. public void Set(BigInteger integer) { - cache = integer; - value = null; + _cache = integer; + _value = ReadOnlyMemory.Empty; // garbage collect the unallocated memory space } /// @@ -185,14 +184,14 @@ public void Set(BigInteger integer) /// The value of the . public void Set(IInteroperable interoperable) { - cache = interoperable; - value = null; + _cache = interoperable; + _value = ReadOnlyMemory.Empty; // garbage collect the unallocated memory space } public static implicit operator BigInteger(StorageItem item) { - item.cache ??= new BigInteger(item.value.Span); - return (BigInteger)item.cache; + item._cache ??= new BigInteger(item._value.Span); + return (BigInteger)item._cache; } public static implicit operator StorageItem(BigInteger value) diff --git a/src/Neo/SmartContract/StorageKey.cs b/src/Neo/SmartContract/StorageKey.cs index 442084fbcd..37ace06165 100644 --- a/src/Neo/SmartContract/StorageKey.cs +++ b/src/Neo/SmartContract/StorageKey.cs @@ -32,7 +32,7 @@ public sealed record StorageKey : IKeySerializable /// public ReadOnlyMemory Key { get; init; } - private byte[] cache = null; + private Memory _cache; // NOTE: StorageKey is readonly, so we can cache the hash code. private int _hashCode = 0; @@ -45,9 +45,9 @@ public StorageKey() { } /// The cached byte array. NOTE: It must be read-only and can be modified by the caller. internal StorageKey(byte[] cache) { - this.cache = cache; - Id = BinaryPrimitives.ReadInt32LittleEndian(cache); - Key = cache.AsMemory(sizeof(int)); + _cache = cache[..].AsMemory(); // allocate new byte array with new memory space + Id = BinaryPrimitives.ReadInt32LittleEndian(_cache.Span); + Key = _cache[sizeof(int)..]; } /// @@ -58,10 +58,10 @@ internal StorageKey(byte[] cache) /// The created search prefix. public static byte[] CreateSearchPrefix(int id, ReadOnlySpan prefix) { - byte[] buffer = new byte[sizeof(int) + prefix.Length]; + Span buffer = stackalloc byte[sizeof(int) + prefix.Length]; BinaryPrimitives.WriteInt32LittleEndian(buffer, id); - prefix.CopyTo(buffer.AsSpan(sizeof(int))); - return buffer; + prefix.CopyTo(buffer[sizeof(int)..]); + return buffer.ToArray(); } public bool Equals(StorageKey other) @@ -82,22 +82,22 @@ public override int GetHashCode() public byte[] ToArray() { - if (cache is null) + if (_cache is { IsEmpty: true }) { - cache = new byte[sizeof(int) + Key.Length]; - BinaryPrimitives.WriteInt32LittleEndian(cache, Id); - Key.CopyTo(cache.AsMemory(sizeof(int))); + _cache = new byte[sizeof(int) + Key.Length]; // allocate new byte array in memory space + BinaryPrimitives.WriteInt32LittleEndian(_cache.Span, Id); + Key.CopyTo(_cache[sizeof(int)..]); } - return cache; + return _cache.ToArray(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator StorageKey(byte[] value) => new StorageKey(value); + public static implicit operator StorageKey(byte[] value) => new(value); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator StorageKey(ReadOnlyMemory value) => new StorageKey(value.Span.ToArray()); + public static implicit operator StorageKey(ReadOnlyMemory value) => new([.. value.Span]); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator StorageKey(ReadOnlySpan value) => new StorageKey(value.ToArray()); + public static implicit operator StorageKey(ReadOnlySpan value) => new([.. value]); } }