diff --git a/csharp/Platform.Data.Doublets.Tests/GarbageCollectionTests.cs b/csharp/Platform.Data.Doublets.Tests/GarbageCollectionTests.cs deleted file mode 100644 index 8d3a27ffb..000000000 --- a/csharp/Platform.Data.Doublets.Tests/GarbageCollectionTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Platform.Data.Doublets.Memory.United.Generic; -using Platform.Memory; -using Xunit; -using TLinkAddress = System.UInt64; - -namespace Platform.Data.Doublets.Tests; - -public class GarbageCollectionTests -{ - public IResizableDirectMemory LinksMemory; - public ILinks Links; - - - public GarbageCollectionTests() - { - LinksMemory = new HeapResizableDirectMemory(); - Links = new UnitedMemoryLinks(LinksMemory); - } - - [Fact] - public void ClearGarbagePartialPointDependency() - { - TLinkAddress link = Links.CreatePoint(); - Links.ClearGarbage(link); - Assert.True(Links.Exists(link)); - } - - [Fact] - public void ClearGarbageFullPointDependency() - { - TLinkAddress link = Links.CreatePoint(); - Links.ClearGarbage(link); - Assert.True(Links.Exists(link)); - } - - [Fact] - public void ClearGarbageWithInDependency() - { - TLinkAddress link = Links.CreatePoint(); - TLinkAddress dependant = Links.GetOrCreate(TLinkAddress.CreateTruncating(10), link); - Links.ClearGarbage(link); - Assert.True(Links.Exists(link)); - Assert.True(Links.Exists(dependant)); - } - - [Fact] - public void ClearGarbageWithOutDependency() - { - TLinkAddress link = Links.CreatePoint(); - TLinkAddress dependant = Links.GetOrCreate(link, TLinkAddress.CreateTruncating(10)); - Links.ClearGarbage(link); - Assert.True(Links.Exists(link)); - Assert.True(Links.Exists(dependant)); - } - - [Fact] - public void ClearGarbageWithoutDependency() - { - TLinkAddress link1 = Links.CreatePoint(); - TLinkAddress link2 = Links.CreatePoint(); - TLinkAddress link3 = Links.GetOrCreate(link1, link2); - Links.ClearGarbage(link3); - Assert.False(Links.Exists(link3)); - Assert.True(Links.Exists(link2)); - Assert.True(Links.Exists(link1)); - } - - [Fact] - public void ComplexgarbageCollectionTest() - { - /* - (1: 1 1) - (2: 2 2) - (3: 1 2) - (4: 3 2) - */ - TLinkAddress link1 = Links.CreatePoint(); - TLinkAddress link2 = Links.CreatePoint(); - TLinkAddress link3 = Links.GetOrCreate(link1, link2); - TLinkAddress link4 = Links.GetOrCreate(link3, link2); - Links.ClearGarbage(link4); - Assert.False(Links.Exists(link4)); - Assert.False(Links.Exists(link3)); - Assert.True(Links.Exists(link2)); - Assert.True(Links.Exists(link1)); - - } -} diff --git a/csharp/Platform.Data.Doublets.Tests/GenericLinksTests.cs b/csharp/Platform.Data.Doublets.Tests/GenericLinksTests.cs deleted file mode 100644 index 5f69bf066..000000000 --- a/csharp/Platform.Data.Doublets.Tests/GenericLinksTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.IO; -using System.Numerics; -using Platform.Data.Doublets.Decorators; -using Xunit; - -using Platform.Memory; - -using Platform.Data.Doublets.Memory.United.Generic; - -namespace Platform.Data.Doublets.Tests -{ - public static class GenericLinksTests - { - [Fact] - public static void CRUDTest() - { - Using(links => links.TestCRUDOperations()); - Using(links => links.TestCRUDOperations()); - Using(links => links.TestCRUDOperations()); - Using(links => links.TestCRUDOperations()); - } - - [Fact] - public static void RawNumbersCRUDTest() - { - Using(links => links.TestRawNumbersCRUDOperations()); - Using(links => links.TestRawNumbersCRUDOperations()); - Using(links => links.TestRawNumbersCRUDOperations()); - Using(links => links.TestRawNumbersCRUDOperations()); - } - - [Fact] - public static void MultipleRandomCreationsAndDeletionsTest() - { - Using(links => links.DecorateWithAutomaticUniquenessAndUsagesResolution().TestMultipleRandomCreationsAndDeletions(16)); // Cannot use more because current implementation of tree cuts out 5 bits from the address space. - Using(links => links.DecorateWithAutomaticUniquenessAndUsagesResolution().TestMultipleRandomCreationsAndDeletions(100)); - Using(links => links.DecorateWithAutomaticUniquenessAndUsagesResolution().TestMultipleRandomCreationsAndDeletions(100)); - Using(links => links.DecorateWithAutomaticUniquenessAndUsagesResolution().TestMultipleRandomCreationsAndDeletions(100)); - } - private static void Using(Action> action) where TLinkAddress : IUnsignedNumber , IShiftOperators, IBitwiseOperators, IMinMaxValue, IComparisonOperators - { - var unitedMemoryLinks = new UnitedMemoryLinks(new HeapResizableDirectMemory()); - using (var logFile = File.Open("linksLogger.txt", FileMode.Create, FileAccess.Write)) - { - LoggingDecorator decoratedStorage = new(unitedMemoryLinks, logFile); - action(decoratedStorage); - } - - /* - File.Delete("db.links"); - using var ffiLinks = new Ffi.Links("db.links"); - action(ffiLinks); - */ - } - } -} diff --git a/csharp/Platform.Data.Doublets.Tests/ILinksBasicTests.cs b/csharp/Platform.Data.Doublets.Tests/ILinksBasicTests.cs deleted file mode 100644 index 5bcc517b7..000000000 --- a/csharp/Platform.Data.Doublets.Tests/ILinksBasicTests.cs +++ /dev/null @@ -1,56 +0,0 @@ - -using System.IO; -using Platform.Data.Doublets.Memory.United.Generic; -using Platform.Memory; -using Xunit; - - -namespace Platform.Data.Doublets.Tests -{ - public static class ILinksBasicTests - { - [Fact] - public static void DeleteAllUsages() - { - var mem = new HeapResizableDirectMemory(); - var links = new UnitedMemoryLinks(mem); - - var root = links.CreatePoint(); - - var a = links.CreatePoint(); - var b = links.CreatePoint(); - - links.CreateAndUpdate(a, root); - links.CreateAndUpdate(b, root); - - Assert.Equal(5U, links.Count()); - - links.DeleteAllUsages(root); - - Assert.Equal(3U, links.Count()); - } - - /* - [Fact] - public static void FfiDeleteAllUsages() - { - File.Delete("db.links"); - var links = new Ffi.Links("db.links"); - - var root = links.CreatePoint(); - - var a = links.CreatePoint(); - var b = links.CreatePoint(); - - links.CreateAndUpdate(a, root); - links.CreateAndUpdate(b, root); - - Assert.Equal(5U, links.Count()); - - links.DeleteAllUsages(root); - - Assert.Equal(3U, links.Count()); - } - */ - } -} diff --git a/csharp/Platform.Data.Doublets.Tests/LinksConstantsTests.cs b/csharp/Platform.Data.Doublets.Tests/LinksConstantsTests.cs deleted file mode 100644 index 29c1d4637..000000000 --- a/csharp/Platform.Data.Doublets.Tests/LinksConstantsTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Xunit; - -namespace Platform.Data.Doublets.Tests -{ - public static class LinksConstantsTests - { - [Fact] - public static void ExternalReferencesTest() - { - LinksConstants constants = new LinksConstants((1, long.MaxValue), (long.MaxValue + 1UL, ulong.MaxValue)); - - //var minimum = new Hybrid(0, isExternal: true); - var minimum = new Hybrid(1, isExternal: true); - var maximum = new Hybrid(long.MaxValue, isExternal: true); - - Assert.True(constants.IsExternalReference(minimum)); - Assert.True(constants.IsExternalReference(maximum)); - } - } -} diff --git a/csharp/Platform.Data.Doublets.Tests/Platform.Data.Doublets.Tests.csproj b/csharp/Platform.Data.Doublets.Tests/Platform.Data.Doublets.Tests.csproj deleted file mode 100644 index 27af73fe0..000000000 --- a/csharp/Platform.Data.Doublets.Tests/Platform.Data.Doublets.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net8 - false - true - latest - enable - - - - - - - - - - - - - - - - - - - - diff --git a/csharp/Platform.Data.Doublets.Tests/ResizableDirectMemoryLinksTests.cs b/csharp/Platform.Data.Doublets.Tests/ResizableDirectMemoryLinksTests.cs deleted file mode 100644 index eadbe02b9..000000000 --- a/csharp/Platform.Data.Doublets.Tests/ResizableDirectMemoryLinksTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.IO; -using Xunit; -using Platform.Singletons; -using Platform.Memory; -using Platform.Data.Doublets.Memory.United.Generic; - -namespace Platform.Data.Doublets.Tests -{ - public static class ResizableDirectMemoryLinksTests - { - private static readonly LinksConstants _constants = Default>.Instance; - - [Fact] - public static void BasicFileMappedMemoryTest() - { - var tempFilename = Path.GetTempFileName(); - using (var memoryAdapter = new TemporaryFileMappedResizableDirectMemory()) - { - using (var unitedMemoryLinksStorage = new UnitedMemoryLinks(memoryAdapter)) - { - unitedMemoryLinksStorage.TestBasicMemoryOperations(); - } - } - File.Delete(tempFilename); - } - - [Fact] - public static void BasicHeapMemoryTest() - { - using (var memory = new HeapResizableDirectMemory(UnitedMemoryLinks.DefaultLinksSizeStep)) - using (var memoryAdapter = new UnitedMemoryLinks(memory, UnitedMemoryLinks.DefaultLinksSizeStep)) - { - memoryAdapter.TestBasicMemoryOperations(); - } - } - private static void TestBasicMemoryOperations(this ILinks memoryAdapter) - { - var link = memoryAdapter.Create(); - memoryAdapter.Delete(link); - } - - [Fact] - public static void NonexistentReferencesHeapMemoryTest() - { - using (var memory = new HeapResizableDirectMemory(UnitedMemoryLinks.DefaultLinksSizeStep)) - using (var memoryAdapter = new UnitedMemoryLinks(memory, UnitedMemoryLinks.DefaultLinksSizeStep)) - { - memoryAdapter.TestNonexistentReferences(); - } - } - private static void TestNonexistentReferences(this ILinks memoryAdapter) - { - var link = memoryAdapter.Create(); - memoryAdapter.Update(link, ulong.MaxValue, ulong.MaxValue); - var resultLink = _constants.Null; - memoryAdapter.Each(foundLink => - { - resultLink = memoryAdapter.GetIndex(foundLink); - return _constants.Break; - }, _constants.Any, ulong.MaxValue, ulong.MaxValue); - Assert.True(resultLink == link); - Assert.True(memoryAdapter.Count(ulong.MaxValue) == 0); - memoryAdapter.Delete(link); - } - } -} diff --git a/csharp/Platform.Data.Doublets.Tests/ScopeTests.cs b/csharp/Platform.Data.Doublets.Tests/ScopeTests.cs deleted file mode 100644 index 028fe9dd6..000000000 --- a/csharp/Platform.Data.Doublets.Tests/ScopeTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Xunit; -using Platform.Scopes; -using Platform.Memory; -using Platform.Data.Doublets.Decorators; -using Platform.Reflection; -using Platform.Data.Doublets.Memory.United.Generic; -using TLinkAddress = System.UInt64; - -namespace Platform.Data.Doublets.Tests -{ - public static class ScopeTests - { - [Fact] - public static void SingleDependencyTest() - { - using (var scope = new Scope()) - { - scope.IncludeAssemblyOf(); - var instance = scope.Use(); - Assert.IsType(instance); - } - } - - [Fact] - public static void CascadeDependencyTest() - { - using (var scope = new Scope()) - { - scope.Include(); - scope.Include>(); - var instance = scope.Use>(); - Assert.IsType>(instance); - } - } - - [Fact(Skip = "Would be fixed later.")] - public static void FullAutoResolutionTest() - { - using (var scope = new Scope(autoInclude: true, autoExplore: true)) - { - var instance = scope.Use>(); - Assert.IsType(instance); - } - } - - [Fact] - public static void TypeParametersTest() - { - using (var scope = new Scope>>()) - { - var links = scope.Use>(); - Assert.IsType>(links); - } - } - } -} diff --git a/csharp/Platform.Data.Doublets.Tests/SplitMemoryGenericLinksTests.cs b/csharp/Platform.Data.Doublets.Tests/SplitMemoryGenericLinksTests.cs deleted file mode 100644 index 8bea34c8f..000000000 --- a/csharp/Platform.Data.Doublets.Tests/SplitMemoryGenericLinksTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Numerics; -using Xunit; -using Platform.Memory; -using Platform.Data.Doublets.Memory.Split.Generic; -using Platform.Data.Doublets.Memory; - -namespace Platform.Data.Doublets.Tests -{ - public unsafe static class SplitMemoryGenericLinksTests - { - [Fact] - public static void CRUDTest() - { - Using(links => links.TestCRUDOperations()); - Using(links => links.TestCRUDOperations()); - Using(links => links.TestCRUDOperations()); - Using(links => links.TestCRUDOperations()); - } - - [Fact] - public static void RawNumbersCRUDTest() - { - UsingWithExternalReferences(links => links.TestRawNumbersCRUDOperations()); - UsingWithExternalReferences(links => links.TestRawNumbersCRUDOperations()); - UsingWithExternalReferences(links => links.TestRawNumbersCRUDOperations()); - UsingWithExternalReferences(links => links.TestRawNumbersCRUDOperations()); - } - - [Fact] - public static void MultipleRandomCreationsAndDeletionsTest() - { - Using(links => links.DecorateWithAutomaticUniquenessAndUsagesResolution().TestMultipleRandomCreationsAndDeletions(16)); // Cannot use more because current implementation of tree cuts out 5 bits from the address space. - Using(links => links.DecorateWithAutomaticUniquenessAndUsagesResolution().TestMultipleRandomCreationsAndDeletions(100)); - Using(links => links.DecorateWithAutomaticUniquenessAndUsagesResolution().TestMultipleRandomCreationsAndDeletions(100)); - Using(links => links.DecorateWithAutomaticUniquenessAndUsagesResolution().TestMultipleRandomCreationsAndDeletions(100)); - } - private static void Using (Action> action) where TLinkAddress : IUnsignedNumber, IComparisonOperators - { - using (var dataMemory = new HeapResizableDirectMemory()) - using (var indexMemory = new HeapResizableDirectMemory()) - using (var memory = new SplitMemoryLinks(dataMemory, indexMemory)) - { - action(memory); - } - } - private static void UsingWithExternalReferences(Action> action) where TLinkAddress : IUnsignedNumber, IComparisonOperators - { - var contants = new LinksConstants(enableExternalReferencesSupport: true); - using (var dataMemory = new HeapResizableDirectMemory()) - using (var indexMemory = new HeapResizableDirectMemory()) - using (var memory = new SplitMemoryLinks(dataMemory, indexMemory, SplitMemoryLinks.DefaultLinksSizeStep, contants)) - { - action(memory); - } - } - } -} diff --git a/csharp/Platform.Data.Doublets/Platform.Data.Doublets.csproj b/csharp/Platform.Data.Doublets/Platform.Data.Doublets.csproj index 7bb9ba27f..c553ec838 100644 --- a/csharp/Platform.Data.Doublets/Platform.Data.Doublets.csproj +++ b/csharp/Platform.Data.Doublets/Platform.Data.Doublets.csproj @@ -4,7 +4,7 @@ LinksPlatform's Platform.Data.Doublets Class Library konard, FreePhoenix888 Platform.Data.Doublets - 0.18.1 + 0.19.0 konard, FreePhoenix888 net8 true @@ -25,7 +25,7 @@ true snupkg latest - Update target framework from net7 to net8. + Add cached sequence walker/reader functionality. Caches relationship between subsequence elements arrays and their addresses for significant performance improvements in sequence reading operations. Includes both in-memory and file-based caching with automatic invalidation. enable diff --git a/csharp/Platform.Data.Doublets/Sequences/CachedSequenceExtensions.cs b/csharp/Platform.Data.Doublets/Sequences/CachedSequenceExtensions.cs new file mode 100644 index 000000000..636e6795b --- /dev/null +++ b/csharp/Platform.Data.Doublets/Sequences/CachedSequenceExtensions.cs @@ -0,0 +1,164 @@ +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Platform.Data.Doublets.Sequences +{ + /// + /// + /// Provides extension methods for cached sequence operations on ILinks. + /// + /// + /// These extensions make it easy to use cached sequence reading functionality with any ILinks implementation. + /// + /// + public static class CachedSequenceExtensions + { + /// + /// Creates a cached sequence walker decorator for the given links storage. + /// + /// The type of link address. + /// The links storage to decorate. + /// Whether to enable file-based caching. + /// The directory for file cache. If null, uses temp directory. + /// The duration to keep file cache. Default is 1 second. + /// A cached sequence walker decorator. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CachedSequenceWalker WithSequenceCache( + this ILinks links, + bool enableFileCache = true, + string? cacheDirectory = null, + TimeSpan? fileCacheDuration = null) + where TLinkAddress : IUnsignedNumber + { + return new CachedSequenceWalker(links, enableFileCache, cacheDirectory, fileCacheDuration); + } + + /// + /// Gets a sequence as an array of elements using cached sequence walker. + /// + /// The type of link address. + /// The links storage. + /// The sequence address. + /// The sequence elements array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TLinkAddress[] GetCachedSequenceArray( + this ILinks links, + TLinkAddress sequenceAddress) + where TLinkAddress : IUnsignedNumber + { + if (links is CachedSequenceWalker cachedWalker) + { + return cachedWalker.GetSequenceArray(sequenceAddress); + } + + // If not already a cached walker, create a temporary one + using var walker = links.WithSequenceCache(); + return walker.GetSequenceArray(sequenceAddress); + } + + /// + /// Gets a sequence as an array of elements asynchronously using cached sequence walker. + /// + /// The type of link address. + /// The links storage. + /// The sequence address. + /// A task that represents the asynchronous operation containing the sequence elements array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task GetCachedSequenceArrayAsync( + this ILinks links, + TLinkAddress sequenceAddress) + where TLinkAddress : IUnsignedNumber + { + if (links is CachedSequenceWalker cachedWalker) + { + return await cachedWalker.GetSequenceArrayAsync(sequenceAddress).ConfigureAwait(false); + } + + // If not already a cached walker, create a temporary one + using var walker = links.WithSequenceCache(); + return await walker.GetSequenceArrayAsync(sequenceAddress).ConfigureAwait(false); + } + + /// + /// Tries to get a cached sequence address for the given elements. + /// + /// The type of link address. + /// The links storage. + /// The sequence elements. + /// The found address, if any. + /// True if a cached address was found, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetCachedSequenceAddress( + this ILinks links, + TLinkAddress[] elements, + out TLinkAddress address) + where TLinkAddress : IUnsignedNumber + { + address = default; + + if (links is CachedSequenceWalker cachedWalker) + { + return cachedWalker.TryGetCachedSequenceAddress(elements, out address); + } + + // If not a cached walker, we can't get cached address + return false; + } + + /// + /// Invalidates cached data for a sequence. + /// + /// The type of link address. + /// The links storage. + /// The sequence address to invalidate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InvalidateCachedSequence( + this ILinks links, + TLinkAddress sequenceAddress) + where TLinkAddress : IUnsignedNumber + { + if (links is CachedSequenceWalker cachedWalker) + { + cachedWalker.InvalidateSequence(sequenceAddress); + } + } + + /// + /// Clears all cached sequence data. + /// + /// The type of link address. + /// The links storage. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ClearSequenceCache(this ILinks links) + where TLinkAddress : IUnsignedNumber + { + if (links is CachedSequenceWalker cachedWalker) + { + cachedWalker.ClearCache(); + } + } + + /// + /// Gets cache statistics for sequence operations. + /// + /// The type of link address. + /// The links storage. + /// A tuple containing hit count, miss count, and hit ratio, or null if not a cached walker. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (long hitCount, long missCount, double hitRatio)? GetSequenceCacheStatistics( + this ILinks links) + where TLinkAddress : IUnsignedNumber + { + if (links is CachedSequenceWalker cachedWalker) + { + return cachedWalker.GetCacheStatistics(); + } + + return null; + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Data.Doublets/Sequences/CachedSequenceWalker.cs b/csharp/Platform.Data.Doublets/Sequences/CachedSequenceWalker.cs new file mode 100644 index 000000000..5deda6271 --- /dev/null +++ b/csharp/Platform.Data.Doublets/Sequences/CachedSequenceWalker.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Platform.Collections.Lists; +using Platform.Data.Doublets.Decorators; +using Platform.Delegates; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Platform.Data.Doublets.Sequences +{ + /// + /// + /// Represents a cached sequence walker/reader that optimizes sequence operations through caching. + /// + /// + /// The link is two subsequences, we can cache the relationship between each array of subsequence's elements and its address. + /// So if need to convert the address to array of elements second time we can do it faster, because of cached arrays. + /// This can have a significant impact on the speed of reading of data from Links storage. + /// + /// + /// The type of link address. + public class CachedSequenceWalker : LinksDisposableDecoratorBase + where TLinkAddress : IUnsignedNumber + { + private readonly SequenceCache _memoryCache; + private readonly SequenceFileCache? _fileCache; + private readonly bool _enableFileCache; + + /// + /// Gets the in-memory cache instance. + /// + public SequenceCache MemoryCache => _memoryCache; + + /// + /// Gets the file cache instance, if enabled. + /// + public SequenceFileCache? FileCache => _fileCache; + + /// + /// Gets a value indicating whether file caching is enabled. + /// + public bool IsFileCacheEnabled => _enableFileCache; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying links storage. + /// Whether to enable file-based caching. + /// The directory for file cache. If null, uses temp directory. + /// The duration to keep file cache. Default is 1 second. + public CachedSequenceWalker( + ILinks links, + bool enableFileCache = true, + string? cacheDirectory = null, + TimeSpan? fileCacheDuration = null) : base(links) + { + _memoryCache = new SequenceCache(); + _enableFileCache = enableFileCache; + + if (_enableFileCache) + { + _fileCache = new SequenceFileCache(cacheDirectory, fileCacheDuration); + } + } + + /// + /// Gets a sequence as an array of elements, using cache when possible. + /// + /// The sequence address. + /// The sequence elements array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TLinkAddress[] GetSequenceArray(TLinkAddress sequence) + { + // Try memory cache first + if (_memoryCache.TryGetSequence(sequence, out var cachedSequence) && cachedSequence != null) + { + return cachedSequence; + } + + // Try file cache if enabled + if (_enableFileCache && _fileCache != null && + _fileCache.TryGetSequence(sequence, out var fileCachedSequence) && fileCachedSequence != null) + { + // Store in memory cache for faster future access + _memoryCache.CacheSequence(sequence, fileCachedSequence); + return fileCachedSequence; + } + + // Cache miss - need to walk the sequence + var elements = WalkSequenceToArray(sequence); + + // Cache the result + CacheSequenceElements(sequence, elements); + + return elements; + } + + /// + /// Gets a sequence as an array of elements asynchronously, using cache when possible. + /// + /// The sequence address. + /// A task that represents the asynchronous operation containing the sequence elements array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async Task GetSequenceArrayAsync(TLinkAddress sequence) + { + // Try memory cache first + if (_memoryCache.TryGetSequence(sequence, out var cachedSequence) && cachedSequence != null) + { + return cachedSequence; + } + + // Try file cache if enabled + if (_enableFileCache && _fileCache != null && + _fileCache.TryGetSequence(sequence, out var fileCachedSequence) && fileCachedSequence != null) + { + // Store in memory cache for faster future access + _memoryCache.CacheSequence(sequence, fileCachedSequence); + return fileCachedSequence; + } + + // Cache miss - need to walk the sequence + var elements = WalkSequenceToArray(sequence); + + // Cache the result asynchronously + await CacheSequenceElementsAsync(sequence, elements).ConfigureAwait(false); + + return elements; + } + + /// + /// Tries to find an existing sequence address for the given elements, using cache when possible. + /// + /// The sequence elements. + /// The found address, if any. + /// True if a cached address was found, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetCachedSequenceAddress(TLinkAddress[] elements, out TLinkAddress address) + { + return _memoryCache.TryGetAddress(elements, out address); + } + + /// + /// Walks a sequence and converts it to an array of elements. + /// + /// The sequence address. + /// The sequence elements array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private TLinkAddress[] WalkSequenceToArray(TLinkAddress sequence) + { + var elements = new List(); + + // Walk the sequence using the standard doublets pattern + // A sequence is typically represented as nested pairs: (element1, (element2, (element3, ...))) + WalkSequenceRecursive(sequence, elements); + + return elements.ToArray(); + } + + /// + /// Recursively walks a sequence structure and collects elements. + /// + /// The current link to examine. + /// The list to collect elements into. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WalkSequenceRecursive(TLinkAddress current, IList elements) + { + if (!_links.Exists(current)) + { + return; + } + + var link = _links.GetLink(current); + var source = _links.GetSource(link); + var target = _links.GetTarget(link); + + // Check if this is a point (self-referencing link) + if (source == current && target == current) + { + elements.Add(current); + return; + } + + // If source is different from current, it's likely an element + if (source != current) + { + // Check if source is a point or should be treated as an element + var sourceLink = _links.GetLink(source); + var sourceSource = _links.GetSource(sourceLink); + var sourceTarget = _links.GetTarget(sourceLink); + + if (sourceSource == source && sourceTarget == source) + { + // Source is a point, add it as element + elements.Add(source); + } + else + { + // Source might be a nested sequence, walk it recursively + WalkSequenceRecursive(source, elements); + } + } + + // Process target similarly + if (target != current) + { + var targetLink = _links.GetLink(target); + var targetSource = _links.GetSource(targetLink); + var targetTarget = _links.GetTarget(targetLink); + + if (targetSource == target && targetTarget == target) + { + // Target is a point, add it as element + elements.Add(target); + } + else + { + // Target might be a nested sequence, walk it recursively + WalkSequenceRecursive(target, elements); + } + } + } + + /// + /// Caches sequence elements in both memory and file cache. + /// + /// The sequence address. + /// The sequence elements. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CacheSequenceElements(TLinkAddress address, TLinkAddress[] elements) + { + if (elements.Length == 0) + { + return; + } + + // Cache in memory + _memoryCache.CacheSequence(address, elements); + + // Cache in file if enabled + if (_enableFileCache && _fileCache != null) + { + _fileCache.CacheSequence(address, elements); + } + } + + /// + /// Asynchronously caches sequence elements in both memory and file cache. + /// + /// The sequence address. + /// The sequence elements. + /// A task representing the asynchronous caching operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private async Task CacheSequenceElementsAsync(TLinkAddress address, TLinkAddress[] elements) + { + if (elements.Length == 0) + { + return; + } + + // Cache in memory + _memoryCache.CacheSequence(address, elements); + + // Cache in file if enabled + if (_enableFileCache && _fileCache != null) + { + await _fileCache.CacheSequenceAsync(address, elements).ConfigureAwait(false); + } + } + + /// + /// Invalidates cached data for a sequence. + /// + /// The sequence address to invalidate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void InvalidateSequence(TLinkAddress address) + { + _memoryCache.RemoveSequence(address); + + if (_enableFileCache && _fileCache != null) + { + _fileCache.RemoveSequence(address); + } + } + + /// + /// Clears all cached data. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearCache() + { + _memoryCache.Clear(); + + if (_enableFileCache && _fileCache != null) + { + _fileCache.Clear(); + } + } + + /// + /// Gets cache statistics. + /// + /// A tuple containing hit count, miss count, and hit ratio. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (long hitCount, long missCount, double hitRatio) GetCacheStatistics() + { + return (_memoryCache.HitCount, _memoryCache.MissCount, _memoryCache.HitRatio); + } + + // Override decorator methods to invalidate cache when data changes + + /// + /// Updates a link and invalidates related cache entries. + /// + public override TLinkAddress Update(IList? restriction, IList? substitution, WriteHandler? handler) + { + var result = base.Update(restriction, substitution, handler); + + // Invalidate cache for updated links + if (restriction != null && restriction.Count > 0) + { + InvalidateSequence(restriction[0]); + } + + return result; + } + + /// + /// Deletes a link and invalidates related cache entries. + /// + public override TLinkAddress Delete(IList? restriction, WriteHandler? handler) + { + // Invalidate cache for deleted links + if (restriction != null && restriction.Count > 0) + { + InvalidateSequence(restriction[0]); + } + + return base.Delete(restriction, handler); + } + + /// + /// Disposes the cached sequence walker and its resources. + /// + protected override void Dispose(bool manual, bool wasDisposed) + { + if (!wasDisposed && manual) + { + _fileCache?.Dispose(); + } + base.Dispose(manual, wasDisposed); + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Data.Doublets/Sequences/SequenceCache.cs b/csharp/Platform.Data.Doublets/Sequences/SequenceCache.cs new file mode 100644 index 000000000..f8c8312c3 --- /dev/null +++ b/csharp/Platform.Data.Doublets/Sequences/SequenceCache.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Platform.Data.Doublets.Sequences +{ + /// + /// + /// Represents an in-memory cache for sequence address-to-array mappings. + /// + /// + /// The link is two subsequences, we can cache the relationship between each array of subsequence's elements and its address. + /// So if need to convert the address to array of elements second time we can do it faster, because of cached arrays. + /// + /// + /// The type of link address. + public class SequenceCache where TLinkAddress : IUnsignedNumber + { + private readonly ConcurrentDictionary _addressToSequenceCache; + private readonly ConcurrentDictionary _sequenceHashToAddressCache; + private readonly object _lock = new(); + private long _hitCount; + private long _missCount; + + /// + /// Gets the number of cache hits. + /// + public long HitCount => _hitCount; + + /// + /// Gets the number of cache misses. + /// + public long MissCount => _missCount; + + /// + /// Gets the cache hit ratio. + /// + public double HitRatio => _hitCount + _missCount > 0 ? (double)_hitCount / (_hitCount + _missCount) : 0; + + /// + /// Gets the current size of the cache. + /// + public int Size => _addressToSequenceCache.Count; + + /// + /// Initializes a new instance of the class. + /// + public SequenceCache() + { + _addressToSequenceCache = new ConcurrentDictionary(); + _sequenceHashToAddressCache = new ConcurrentDictionary(); + } + + /// + /// Tries to get a cached sequence by its address. + /// + /// The sequence address. + /// The cached sequence if found. + /// True if the sequence was found in cache, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetSequence(TLinkAddress address, out TLinkAddress[]? sequence) + { + if (_addressToSequenceCache.TryGetValue(address, out var cachedSequence)) + { + sequence = cachedSequence; + Interlocked.Increment(ref _hitCount); + return true; + } + + sequence = null; + Interlocked.Increment(ref _missCount); + return false; + } + + /// + /// Tries to get a cached address by sequence elements hash. + /// + /// The sequence elements. + /// The cached address if found. + /// True if the address was found in cache, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetAddress(TLinkAddress[] sequence, out TLinkAddress address) + { + var hash = ComputeSequenceHash(sequence); + if (_sequenceHashToAddressCache.TryGetValue(hash, out address)) + { + Interlocked.Increment(ref _hitCount); + return true; + } + + address = default; + Interlocked.Increment(ref _missCount); + return false; + } + + /// + /// Caches a sequence and its address. + /// + /// The sequence address. + /// The sequence elements. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CacheSequence(TLinkAddress address, TLinkAddress[] sequence) + { + if (sequence == null || sequence.Length == 0) + { + return; + } + + var sequenceCopy = new TLinkAddress[sequence.Length]; + Array.Copy(sequence, sequenceCopy, sequence.Length); + + var hash = ComputeSequenceHash(sequenceCopy); + + _addressToSequenceCache.TryAdd(address, sequenceCopy); + _sequenceHashToAddressCache.TryAdd(hash, address); + } + + /// + /// Removes a sequence from cache. + /// + /// The sequence address to remove. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveSequence(TLinkAddress address) + { + if (_addressToSequenceCache.TryRemove(address, out var sequence)) + { + var hash = ComputeSequenceHash(sequence); + _sequenceHashToAddressCache.TryRemove(hash, out _); + } + } + + /// + /// Clears all cached sequences. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + _addressToSequenceCache.Clear(); + _sequenceHashToAddressCache.Clear(); + + lock (_lock) + { + _hitCount = 0; + _missCount = 0; + } + } + + /// + /// Computes a hash for a sequence of elements. + /// + /// The sequence elements. + /// The hash string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string ComputeSequenceHash(TLinkAddress[] sequence) + { + var hash = new HashCode(); + foreach (var element in sequence) + { + hash.Add(element); + } + return hash.ToHashCode().ToString(); + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Data.Doublets/Sequences/SequenceFileCache.cs b/csharp/Platform.Data.Doublets/Sequences/SequenceFileCache.cs new file mode 100644 index 000000000..a473bf20a --- /dev/null +++ b/csharp/Platform.Data.Doublets/Sequences/SequenceFileCache.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Platform.Data.Doublets.Sequences +{ + /// + /// + /// Represents a file-based cache for sequences with expiration support. + /// + /// + /// This file should be created each time the sequence that does not have such file is read. + /// We also can store a file with read sequence for a cache duration (1 sec or greater). + /// + /// + /// The type of link address. + public class SequenceFileCache : IDisposable where TLinkAddress : IUnsignedNumber + { + private readonly string _cacheDirectory; + private readonly TimeSpan _cacheDuration; + private readonly ConcurrentDictionary _addressToFilePathCache; + private readonly Timer _cleanupTimer; + private readonly object _disposeLock = new(); + private bool _disposed; + + /// + /// Gets the cache directory path. + /// + public string CacheDirectory => _cacheDirectory; + + /// + /// Gets the cache duration. + /// + public TimeSpan CacheDuration => _cacheDuration; + + /// + /// Initializes a new instance of the class. + /// + /// The directory to store cache files. + /// The duration to keep cache files. Default is 1 second. + public SequenceFileCache(string cacheDirectory = null, TimeSpan? cacheDuration = null) + { + _cacheDirectory = cacheDirectory ?? Path.Combine(Path.GetTempPath(), "DoubletsSequenceCache"); + _cacheDuration = cacheDuration ?? TimeSpan.FromSeconds(1); + _addressToFilePathCache = new ConcurrentDictionary(); + + // Ensure cache directory exists + if (!Directory.Exists(_cacheDirectory)) + { + Directory.CreateDirectory(_cacheDirectory); + } + + // Start cleanup timer to remove expired files every cache duration + _cleanupTimer = new Timer(CleanupExpiredFiles, null, _cacheDuration, _cacheDuration); + } + + /// + /// Tries to get a cached sequence from file. + /// + /// The sequence address. + /// The cached sequence if found and not expired. + /// True if the sequence was found and is still valid, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetSequence(TLinkAddress address, out TLinkAddress[]? sequence) + { + sequence = null; + + if (_disposed) + { + return false; + } + + var filePath = GetCacheFilePath(address); + + if (!File.Exists(filePath)) + { + return false; + } + + try + { + var fileInfo = new FileInfo(filePath); + if (DateTime.UtcNow - fileInfo.LastWriteTimeUtc > _cacheDuration) + { + // File is expired, remove it + File.Delete(filePath); + _addressToFilePathCache.TryRemove(address, out _); + return false; + } + + var json = File.ReadAllText(filePath); + var cacheEntry = JsonSerializer.Deserialize(json); + + if (cacheEntry?.Elements != null) + { + sequence = cacheEntry.Elements.Select(ConvertFromString).ToArray(); + return true; + } + } + catch (Exception) + { + // If there's any error reading the file, consider it as cache miss + // and try to remove the corrupted file + try + { + File.Delete(filePath); + _addressToFilePathCache.TryRemove(address, out _); + } + catch + { + // Ignore cleanup errors + } + } + + return false; + } + + /// + /// Caches a sequence to file asynchronously. + /// + /// The sequence address. + /// The sequence elements. + /// A task representing the asynchronous operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async Task CacheSequenceAsync(TLinkAddress address, TLinkAddress[] sequence) + { + if (_disposed || sequence == null || sequence.Length == 0) + { + return; + } + + try + { + var filePath = GetCacheFilePath(address); + var cacheEntry = new SequenceCacheEntry + { + Address = ConvertToString(address), + Elements = sequence.Select(ConvertToString).ToArray(), + CreatedAt = DateTime.UtcNow + }; + + var json = JsonSerializer.Serialize(cacheEntry, new JsonSerializerOptions + { + WriteIndented = false + }); + + await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false); + _addressToFilePathCache.TryAdd(address, filePath); + } + catch (Exception) + { + // Ignore caching errors - the system should work without file cache + } + } + + /// + /// Caches a sequence to file synchronously. + /// + /// The sequence address. + /// The sequence elements. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CacheSequence(TLinkAddress address, TLinkAddress[] sequence) + { + if (_disposed || sequence == null || sequence.Length == 0) + { + return; + } + + try + { + var filePath = GetCacheFilePath(address); + var cacheEntry = new SequenceCacheEntry + { + Address = ConvertToString(address), + Elements = sequence.Select(ConvertToString).ToArray(), + CreatedAt = DateTime.UtcNow + }; + + var json = JsonSerializer.Serialize(cacheEntry, new JsonSerializerOptions + { + WriteIndented = false + }); + + File.WriteAllText(filePath, json); + _addressToFilePathCache.TryAdd(address, filePath); + } + catch (Exception) + { + // Ignore caching errors - the system should work without file cache + } + } + + /// + /// Removes a cached sequence file. + /// + /// The sequence address. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveSequence(TLinkAddress address) + { + if (_disposed) + { + return; + } + + var filePath = GetCacheFilePath(address); + + try + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + _addressToFilePathCache.TryRemove(address, out _); + } + catch (Exception) + { + // Ignore cleanup errors + } + } + + /// + /// Clears all cached files. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + if (_disposed) + { + return; + } + + try + { + if (Directory.Exists(_cacheDirectory)) + { + var files = Directory.GetFiles(_cacheDirectory, "seq_*.json"); + foreach (var file in files) + { + try + { + File.Delete(file); + } + catch (Exception) + { + // Ignore individual file deletion errors + } + } + } + _addressToFilePathCache.Clear(); + } + catch (Exception) + { + // Ignore cleanup errors + } + } + + /// + /// Gets the cache file path for a given address. + /// + /// The sequence address. + /// The file path for caching the sequence. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string GetCacheFilePath(TLinkAddress address) + { + if (_addressToFilePathCache.TryGetValue(address, out var cachedPath)) + { + return cachedPath; + } + + var fileName = $"seq_{ConvertToString(address)}.json"; + return Path.Combine(_cacheDirectory, fileName); + } + + /// + /// Cleanup timer callback to remove expired files. + /// + /// Timer state (unused). + private void CleanupExpiredFiles(object? state) + { + if (_disposed) + { + return; + } + + try + { + if (!Directory.Exists(_cacheDirectory)) + { + return; + } + + var files = Directory.GetFiles(_cacheDirectory, "seq_*.json"); + var now = DateTime.UtcNow; + + foreach (var file in files) + { + try + { + var fileInfo = new FileInfo(file); + if (now - fileInfo.LastWriteTimeUtc > _cacheDuration) + { + File.Delete(file); + } + } + catch (Exception) + { + // Ignore individual file cleanup errors + } + } + } + catch (Exception) + { + // Ignore cleanup errors + } + } + + /// + /// Converts TLinkAddress to string for serialization. + /// + /// The address to convert. + /// String representation of the address. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string ConvertToString(TLinkAddress address) + { + return address.ToString() ?? ""; + } + + /// + /// Converts string back to TLinkAddress for deserialization. + /// + /// The string value to convert. + /// The converted address. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TLinkAddress ConvertFromString(string value) + { + return TLinkAddress.Parse(value, null); + } + + /// + /// Disposes the file cache and cleanup timer. + /// + public void Dispose() + { + lock (_disposeLock) + { + if (!_disposed) + { + _disposed = true; + _cleanupTimer?.Dispose(); + } + } + } + + /// + /// Represents a cache entry for serialization. + /// + private class SequenceCacheEntry + { + public string Address { get; set; } = ""; + public string[] Elements { get; set; } = Array.Empty(); + public DateTime CreatedAt { get; set; } + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Data.Doublets/Tests/ILinksExtensions.cs b/csharp/Platform.Data.Doublets/Tests/ILinksExtensions.cs deleted file mode 100644 index c71274046..000000000 --- a/csharp/Platform.Data.Doublets/Tests/ILinksExtensions.cs +++ /dev/null @@ -1,384 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using Platform.Ranges; -using Platform.Random; -using Platform.Converters; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Platform.Numbers; -using Platform.Setters; -using Range = System.Range; - -namespace Platform.Data.Doublets.Tests -{ - /// - /// - /// Represents the links extensions. - /// - /// - /// - public static class ILinksExtensions - { - private static void EnsureTrue(bool boolean) - { - EnsureTrue(boolean, default); - } - - private static void EnsureTrue(bool boolean, string message) - { - if (boolean) - { - return; - } - string messageBuilder() => message; - throw new ArgumentException("EnsureTrue Failed. The value is not a true. " + messageBuilder()); - } - - private static void EnsureEqual(T expected, T actual) where T : IUnsignedNumber - { - EnsureEqual(expected, actual, default); - } - - private static void EnsureEqual(T expected, T actual, string message) where T : IUnsignedNumber - { - EnsureEqual(expected, actual, message, EqualityComparer.Default); - } - - private static void EnsureEqual(T expected, T actual, string message, IEqualityComparer equalityComparer) where T : IUnsignedNumber - { - if (expected == actual) - { - return; - } - string messageBuilder() => message; - throw new ArgumentException("EnsureEqual Failed. The values are not equal. " + messageBuilder()); - } - - /// - /// - /// Tests the crud operations using the specified links. - /// - /// - /// - /// - /// The . - /// - /// - /// - /// The links. - /// - /// - public static void TestCRUDOperations(this ILinks links) where T: IUnsignedNumber - { - var constants = links.Constants; - var zero = T.Zero; - var one = T.One; - - // Create Link - EnsureTrue(links.Count() == zero); - var setter = new Setter(constants.Continue, constants.Break, constants.Null); - links.Each(setter.SetFirstFromNonNullListAndReturnTrue, constants.Any, constants.Any, constants.Any); - EnsureTrue(setter.Result == constants.Null); - var linkAddress = links.Create(); - var link = new Link(links.GetLink(linkAddress)); - EnsureTrue(link.Count == 3); - EnsureTrue(link.Index == linkAddress); - EnsureTrue(link.Source == constants.Null); - EnsureTrue(link.Target == constants.Null); - EnsureTrue(links.Count() == one); - - // Get first link - setter = new Setter(constants.Continue, constants.Break, constants.Null); - links.Each(setter.SetFirstFromNonNullListAndReturnTrue, constants.Any, constants.Any, constants.Any); - EnsureTrue(setter.Result == linkAddress); - - // Update link to reference itself - links.Update(linkAddress, linkAddress, linkAddress); - link = new Link(links.GetLink(linkAddress)); - EnsureTrue(link.Source == linkAddress); - EnsureTrue(link.Target == linkAddress); - - // Update link to reference null (prepare for delete) - var updated = links.Update(linkAddress, constants.Null, constants.Null); - EnsureTrue(updated == linkAddress); - link = new Link(links.GetLink(linkAddress)); - EnsureTrue(link.Source == constants.Null); - EnsureTrue(link.Target == constants.Null); - - // Delete link - links.Delete(linkAddress); - EnsureTrue(links.Count() == zero); - setter = new Setter(constants.Continue, constants.Break, constants.Null); - links.Each(setter.SetFirstFromNonNullListAndReturnTrue, constants.Any, constants.Any, constants.Any); - EnsureTrue(setter.Result == constants.Null); - } - - /// - /// - /// Tests the raw numbers crud operations using the specified links. - /// - /// - /// - /// - /// The . - /// - /// - /// - /// The links. - /// - /// - public static void TestRawNumbersCRUDOperations(this ILinks links) where T : IUnsignedNumber - { - // Constants - var constants = links.Constants; - var zero = default(T); - var one = ++zero; - var two = ++one; - var h106E = new Hybrid(106L, isExternal: true); - var h107E = new Hybrid(-char.ConvertFromUtf32(107)[0]); - var h108E = new Hybrid(-108L); - EnsureEqual(T.CreateTruncating(106L) , T.CreateTruncating(h106E.AbsoluteValue) ); - EnsureEqual(T.CreateTruncating(107L), T.CreateTruncating(h107E.AbsoluteValue)); - EnsureEqual(T.CreateTruncating(108L), T.CreateTruncating(h108E.AbsoluteValue)); - - // Create Link (External -> External) - var linkAddress1 = links.Create(); - links.Update(linkAddress1, h106E, h108E); - var link1 = new Link(links.GetLink(linkAddress1)); - EnsureTrue(link1.Source == h106E.Value); - EnsureTrue(link1.Target == h108E.Value); - - // Create Link (Internal -> External) - var linkAddress2 = links.Create(); - links.Update(linkAddress2, linkAddress1, h108E); - var link2 = new Link(links.GetLink(linkAddress2)); - EnsureTrue(link2.Source == linkAddress1); - EnsureTrue(link2.Target == h108E.Value); - - // Create Link (Internal -> Internal) - var linkAddress3 = links.Create(); - links.Update(linkAddress3, linkAddress1, linkAddress2); - var link3 = new Link(links.GetLink(linkAddress3)); - EnsureTrue(link3.Source == linkAddress1); - EnsureTrue(link3.Target == linkAddress2); - - // Search for created link - var setter1 = new Setter(constants.Continue, constants.Break, constants.Null); - links.Each(setter1.SetFirstAndReturnFalse, constants.Any, h106E, h108E); - EnsureTrue(setter1.Result == linkAddress1); - - // Search for nonexistent link - var setter2 = new Setter(constants.Continue, constants.Break, constants.Null); - links.Each(setter2.SetFirstAndReturnFalse, constants.Any, h106E, h107E); - EnsureTrue(setter2.Result == constants.Null); - - // Update link to reference null (prepare for delete) - var updated = links.Update(linkAddress3, constants.Null, constants.Null); - EnsureTrue(updated == linkAddress3); - link3 = new Link(links.GetLink(linkAddress3)); - EnsureTrue(link3.Source == constants.Null); - EnsureTrue(link3.Target == constants.Null); - - // Delete link - links.Delete(linkAddress3); - EnsureTrue(links.Count() == two); - var isLinkAddress2Found = links.All().Any(link => link![constants.IndexPart] == linkAddress2); - EnsureTrue(isLinkAddress2Found); - } - - /// - /// - /// Tests the multiple random creations and deletions using the specified links. - /// - /// - /// - /// - /// The link. - /// - /// - /// - /// The links. - /// - /// - /// - /// The maximum operations per cycle. - /// - /// - public static void TestMultipleRandomCreationsAndDeletions(this ILinks links, int maximumOperationsPerCycle) where TLink : IUnsignedNumber - { - var comparer = Comparer.Default; - var addressToUInt64Converter = CheckedConverter.Default; - for (var N = 1; N < maximumOperationsPerCycle; N++) - { - var random = new System.Random(N); - var createdAddresses = new List(); - var created = 0; - for (var i = 0; i < N; i++) - { - var createPoint = random.NextBoolean(); - if (created >= 2 && createPoint) - { - var linksAddressRange = new Range(0, created); - var source = createdAddresses[random.Next(linksAddressRange.Minimum, linksAddressRange.Maximum)]; - var target = createdAddresses[random.Next(linksAddressRange.Minimum, linksAddressRange.Maximum)]; //-V3086 - var resultLink = links.SearchOrDefault(source, target); - if (resultLink != default) - { - continue; - } - resultLink = links.CreateAndUpdate(source, target); - if (comparer.Compare(resultLink, default) > 0) - { - createdAddresses.Add(resultLink); - created++; - } - } - else - { - createdAddresses.Add(links.Create()); - created++; - } - } - EnsureTrue((ulong)created == addressToUInt64Converter.Convert(links.Count())); - var allLinks = links.All(); - var deletedLinksAddressRange = new Range(0, created); - // Random deletions - for (var i = 0; i < N; i++) - { - var id = createdAddresses[random.Next(deletedLinksAddressRange.Minimum, deletedLinksAddressRange.Maximum)]; - if (links.Exists(id)) - { - links.Delete(id); - } - } - // Delete all remaining links - for (var i = 0; i < createdAddresses.Count; i++) - { - if (links.Exists(createdAddresses[i])) - { - links.Delete(createdAddresses[i]); - } - } - EnsureTrue(addressToUInt64Converter.Convert(links.Count()) == 0L); - } - } - - /// - /// - /// - /// - /// - /// - public static void TestMultipleCreationsAndDeletions(this ILinks links, int numberOfOperations) where TLinkAddress : IUnsignedNumber - { - for (int i = 0; i < numberOfOperations; i++) - { - links.Create(); - } - for (int i = 0; i < numberOfOperations; i++) - { - links.Delete(links.Count()); - } - } - - /// - /// - /// Runs the random creations using the specified links. - /// - /// - /// - /// - /// The link. - /// - /// - /// - /// The links. - /// - /// - /// - /// The amount of creations. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RunRandomCreations(this ILinks links, ulong amountOfCreations) where TLink : IUnsignedNumber - { - var random = RandomHelpers.Default; - for (var i = 0UL; i < amountOfCreations; i++) - { - var linksAddressRange = new Range(ulong.CreateTruncating(TLink.Zero), ulong.CreateTruncating(links.Count())); - var source = TLink.CreateTruncating(random.NextUInt64(linksAddressRange)); - var target = TLink.CreateTruncating(random.NextUInt64(linksAddressRange)); - links.GetOrCreate(source, target); - } - } - - /// - /// - /// Runs the random searches using the specified links. - /// - /// - /// - /// - /// The link. - /// - /// - /// - /// The links. - /// - /// - /// - /// The amount of searches. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RunRandomSearches(this ILinks links, ulong amountOfSearches) where TLink : IUnsignedNumber - { - var random = RandomHelpers.Default; - for (var i = 0UL; i < amountOfSearches; i++) - { - var linksAddressRange = new Range(ulong.CreateTruncating(TLink.Zero), ulong.CreateTruncating(links.Count())); - var source = TLink.CreateTruncating(random.NextUInt64(linksAddressRange)); - var target = TLink.CreateTruncating(random.NextUInt64(linksAddressRange)); - links.SearchOrDefault(source, target); - } - } - - /// - /// - /// Runs the random deletions using the specified links. - /// - /// - /// - /// - /// The link. - /// - /// - /// - /// The links. - /// - /// - /// - /// The amount of deletions. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RunRandomDeletions(this ILinks links, TLink amountOfDeletions) where TLink : IUnsignedNumber, IComparisonOperators - { - var random = RandomHelpers.Default; - var linksCount = links.Count(); - var min = amountOfDeletions > linksCount ? TLink.Zero : linksCount - amountOfDeletions; - for (var i = TLink.Zero; i < amountOfDeletions; i++) - { - linksCount = links.Count(); - if (linksCount <= min) - { - break; - } - var linksAddressRange = new Range(ulong.CreateTruncating(min), ulong.CreateTruncating(linksCount)); - var link = TLink.CreateTruncating(random.NextUInt64(linksAddressRange)); - links.Delete(link); - } - } - } -} diff --git a/examples/CachedSequenceExample.cs b/examples/CachedSequenceExample.cs new file mode 100644 index 000000000..0d5dfa6c8 --- /dev/null +++ b/examples/CachedSequenceExample.cs @@ -0,0 +1,192 @@ +using System; +using Platform.Data.Doublets.Memory.United.Generic; +using Platform.Data.Doublets.Sequences; +using Platform.Memory; + +namespace Platform.Data.Doublets.Examples +{ + /// + /// + /// Example demonstrating how to use the cached sequence walker/reader functionality. + /// + /// + /// The link is two subsequences, we can cache the relationship between each array of subsequence's elements and its address. + /// So if need to convert the address to array of elements second time we can do it faster, because of cached arrays. + /// This can have a significant impact on the speed of reading of data from Links storage. + /// + /// + public class CachedSequenceExample + { + /// + /// Demonstrates basic usage of the cached sequence walker. + /// + public static void BasicUsage() + { + Console.WriteLine("=== Cached Sequence Walker Basic Usage Example ==="); + + using var memory = new HeapResizableDirectMemory(); + using var links = new UnitedMemoryLinks(memory); + + // Create some test data points + var element1 = links.CreatePoint(); + var element2 = links.CreatePoint(); + var element3 = links.CreatePoint(); + + Console.WriteLine($"Created elements: {element1}, {element2}, {element3}"); + + // Create a sequence: element1 -> (element2 -> element3) + var innerPair = links.CreateAndUpdate(element2, element3); + var sequence = links.CreateAndUpdate(element1, innerPair); + + Console.WriteLine($"Created sequence: {sequence}"); + + // Create cached sequence walker + using var cachedWalker = new CachedSequenceWalker(links, enableFileCache: false); + + // First read - will be cache miss and populate cache + var result1 = cachedWalker.GetSequenceArray(sequence); + var stats1 = cachedWalker.GetCacheStatistics(); + + Console.WriteLine($"First read - Sequence elements: [{string.Join(", ", result1)}]"); + Console.WriteLine($"Cache stats after first read: Hits={stats1.hitCount}, Misses={stats1.missCount}, Ratio={stats1.hitRatio:P2}"); + + // Second read - will be cache hit + var result2 = cachedWalker.GetSequenceArray(sequence); + var stats2 = cachedWalker.GetCacheStatistics(); + + Console.WriteLine($"Second read - Sequence elements: [{string.Join(", ", result2)}]"); + Console.WriteLine($"Cache stats after second read: Hits={stats2.hitCount}, Misses={stats2.missCount}, Ratio={stats2.hitRatio:P2}"); + + Console.WriteLine("✓ Performance improvement through caching demonstrated!"); + } + + /// + /// Demonstrates using extension methods for easier access. + /// + public static void ExtensionMethodsUsage() + { + Console.WriteLine("\n=== Extension Methods Usage Example ==="); + + using var memory = new HeapResizableDirectMemory(); + using var links = new UnitedMemoryLinks(memory); + + // Create test data + var element1 = links.CreatePoint(); + var element2 = links.CreatePoint(); + var sequence = links.CreateAndUpdate(element1, element2); + + Console.WriteLine($"Created sequence: {sequence}"); + + // Use extension method - creates temporary cached walker + var result = links.GetCachedSequenceArray(sequence); + + Console.WriteLine($"Using extension method - Sequence elements: [{string.Join(", ", result)}]"); + Console.WriteLine("✓ Extension methods provide convenient access!"); + } + + /// + /// Demonstrates file-based caching functionality. + /// + public static void FileCacheUsage() + { + Console.WriteLine("\n=== File Cache Usage Example ==="); + + using var memory = new HeapResizableDirectMemory(); + using var links = new UnitedMemoryLinks(memory); + + // Create test data + var element1 = links.CreatePoint(); + var element2 = links.CreatePoint(); + var sequence = links.CreateAndUpdate(element1, element2); + + Console.WriteLine($"Created sequence: {sequence}"); + + // Create cached walker with file cache enabled + var cacheDirectory = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "DoubletsSequenceCache"); + using var cachedWalker = new CachedSequenceWalker( + links, + enableFileCache: true, + cacheDirectory: cacheDirectory, + fileCacheDuration: TimeSpan.FromSeconds(10)); + + // First read - will populate both memory and file cache + var result1 = cachedWalker.GetSequenceArray(sequence); + var stats1 = cachedWalker.GetCacheStatistics(); + + Console.WriteLine($"First read - Sequence elements: [{string.Join(", ", result1)}]"); + Console.WriteLine($"Cache directory: {cachedWalker.FileCache?.CacheDirectory}"); + Console.WriteLine($"File cache enabled: {cachedWalker.IsFileCacheEnabled}"); + Console.WriteLine($"Cache duration: {cachedWalker.FileCache?.CacheDuration}"); + + // Clear memory cache but keep file cache + cachedWalker.MemoryCache.Clear(); + + // Second read - will hit file cache and populate memory cache again + var result2 = cachedWalker.GetSequenceArray(sequence); + var stats2 = cachedWalker.GetCacheStatistics(); + + Console.WriteLine($"After memory cache clear - Sequence elements: [{string.Join(", ", result2)}]"); + Console.WriteLine($"Cache stats: Hits={stats2.hitCount}, Misses={stats2.missCount}, Ratio={stats2.hitRatio:P2}"); + Console.WriteLine("✓ File caching provides persistence across memory cache clears!"); + } + + /// + /// Demonstrates cache invalidation on data modifications. + /// + public static void CacheInvalidationExample() + { + Console.WriteLine("\n=== Cache Invalidation Example ==="); + + using var memory = new HeapResizableDirectMemory(); + using var links = new UnitedMemoryLinks(memory); + using var cachedWalker = new CachedSequenceWalker(links, enableFileCache: false); + + // Create test sequence + var element1 = links.CreatePoint(); + var element2 = links.CreatePoint(); + var sequence = links.CreateAndUpdate(element1, element2); + + Console.WriteLine($"Created sequence: {sequence}"); + + // Read sequence to populate cache + var result1 = cachedWalker.GetSequenceArray(sequence); + var stats1 = cachedWalker.GetCacheStatistics(); + + Console.WriteLine($"Initial read - Sequence elements: [{string.Join(", ", result1)}]"); + Console.WriteLine($"Cache stats: Hits={stats1.hitCount}, Misses={stats1.missCount}"); + + // Update the sequence - this should automatically invalidate cache + var element3 = links.CreatePoint(); + cachedWalker.Update(sequence, element1, element3); + + // Read again - should be cache miss due to automatic invalidation + var result2 = cachedWalker.GetSequenceArray(sequence); + var stats2 = cachedWalker.GetCacheStatistics(); + + Console.WriteLine($"After update - Sequence elements: [{string.Join(", ", result2)}]"); + Console.WriteLine($"Cache stats: Hits={stats2.hitCount}, Misses={stats2.missCount}"); + Console.WriteLine("✓ Cache automatically invalidated on data modification!"); + } + + /// + /// Main entry point for the example. + /// + public static void Main() + { + try + { + BasicUsage(); + ExtensionMethodsUsage(); + FileCacheUsage(); + CacheInvalidationExample(); + + Console.WriteLine("\n🎉 All examples completed successfully!"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Example failed: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } + } + } +} \ No newline at end of file