diff --git a/csharp/Platform.Data.Doublets.Tests/SequencesTests.cs b/csharp/Platform.Data.Doublets.Tests/SequencesTests.cs new file mode 100644 index 000000000..bf2a7a63c --- /dev/null +++ b/csharp/Platform.Data.Doublets.Tests/SequencesTests.cs @@ -0,0 +1,206 @@ +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 SequencesTests + { + [Fact] + public static void SequencesWithoutCompactificationTest() + { + Using(links => + { + using var sequences = new Sequences(links, enableAutomaticCompactification: false); + + // Verify that automatic compactification is disabled + Assert.False(sequences.IsAutomaticCompactificationEnabled); + + // Create some links + var link1 = links.Create(); + var link2 = links.Create(); + var link3 = links.GetOrCreate(link1, link2); + + var countBefore = links.Count(); + + // Dispose should not perform compactification + sequences.Dispose(); + + var countAfter = links.Count(); + Assert.Equal(countBefore, countAfter); + }); + } + + [Fact] + public static void SequencesWithCompactificationTest() + { + Using(links => + { + using var sequences = new Sequences(links, enableAutomaticCompactification: true); + + // Verify that automatic compactification is enabled + Assert.True(sequences.IsAutomaticCompactificationEnabled); + + // Create some duplicate links + var link1 = links.Create(); + var link2 = links.Create(); + + // Create the first link with specific source and target + var originalLink = links.GetOrCreate(link1, link2); + + // Create another link with the same source and target (duplicate) + var duplicateLink = links.CreateAndUpdate(link1, link2); + + // Verify we have duplicates + Assert.NotEqual(originalLink, duplicateLink); + Assert.Equal(links.GetSource(originalLink), links.GetSource(duplicateLink)); + Assert.Equal(links.GetTarget(originalLink), links.GetTarget(duplicateLink)); + + var countBefore = links.Count(); + + // Dispose should perform compactification and remove duplicates + sequences.Dispose(); + + var countAfter = links.Count(); + + // Should have fewer links after compactification + Assert.True(countAfter < countBefore, $"Expected count to decrease from {countBefore} to {countAfter}"); + }); + } + + [Fact] + public static void ManualCompactificationTest() + { + Using(links => + { + var sequences = new Sequences(links, enableAutomaticCompactification: false); + + // Create some duplicate links + var link1 = links.Create(); + var link2 = links.Create(); + + // Create the first link with specific source and target + var originalLink = links.GetOrCreate(link1, link2); + + // Create another link with the same source and target (duplicate) + var duplicateLink = links.CreateAndUpdate(link1, link2); + + // Verify we have duplicates + Assert.NotEqual(originalLink, duplicateLink); + + var countBefore = links.Count(); + + // Manually trigger compactification + sequences.Compact(); + + var countAfter = links.Count(); + + // Should have fewer links after compactification + Assert.True(countAfter < countBefore, $"Expected count to decrease from {countBefore} to {countAfter}"); + + sequences.Dispose(); + }); + } + + [Fact] + public static void CompactificationSkipsPointLinksTest() + { + Using(links => + { + var sequences = new Sequences(links, enableAutomaticCompactification: true); + + // Create point links (links that reference themselves) + var pointLink1 = links.CreatePoint(); + var pointLink2 = links.CreatePoint(); + + // Create regular duplicate links + var link1 = links.Create(); + var link2 = links.Create(); + var originalLink = links.GetOrCreate(link1, link2); + var duplicateLink = links.CreateAndUpdate(link1, link2); + + var countBefore = links.Count(); + var pointLinksCountBefore = CountPointLinks(links); + + // Compactification should skip point links + sequences.Compact(); + + var countAfter = links.Count(); + var pointLinksCountAfter = CountPointLinks(links); + + // Point links should remain unchanged + Assert.Equal(pointLinksCountBefore, pointLinksCountAfter); + + // But duplicates should be removed + Assert.True(countAfter < countBefore, $"Expected count to decrease from {countBefore} to {countAfter}"); + + sequences.Dispose(); + }); + } + + [Fact] + public static void MultipleDisposeCallsTest() + { + Using(links => + { + var sequences = new Sequences(links, enableAutomaticCompactification: true); + + // Create some links + var link1 = links.Create(); + var link2 = links.Create(); + links.GetOrCreate(link1, link2); + + // Multiple dispose calls should be safe + sequences.Dispose(); + sequences.Dispose(); // This should not throw or cause issues + + // Verify links are still accessible + Assert.True(links.Count() > 0); + }); + } + + private static uint CountPointLinks(ILinks links) + where TLinkAddress : IUnsignedNumber + { + uint count = 0; + var constants = links.Constants; + var query = new Link(constants.Any, constants.Any, constants.Any); + + links.Each(link => + { + var linkAddress = links.GetIndex(link); + var source = links.GetSource(link); + var target = links.GetTarget(link); + + if (source == linkAddress && target == linkAddress) + { + count++; + } + + return constants.Continue; + }, query); + + return count; + } + + private static void Using(Action> action) + where TLinkAddress : IUnsignedNumber, IShiftOperators, + IBitwiseOperators, IMinMaxValue, + IComparisonOperators + { + var unitedMemoryLinks = new UnitedMemoryLinks(new HeapResizableDirectMemory()); + try + { + action(unitedMemoryLinks); + } + finally + { + unitedMemoryLinks?.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Data.Doublets/Sequences.cs b/csharp/Platform.Data.Doublets/Sequences.cs new file mode 100644 index 000000000..81fe3a5d3 --- /dev/null +++ b/csharp/Platform.Data.Doublets/Sequences.cs @@ -0,0 +1,163 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using Platform.Data.Doublets.Decorators; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Platform.Data.Doublets; + +/// +/// +/// Represents a sequences manager that provides automatic compactification (deduplication) functionality. +/// +/// +/// This class decorates an underlying ILinks instance and optionally performs automatic +/// compactification when disposed, merging duplicate links that have the same source and target. +/// +/// +/// Compactification helps reduce storage overhead by eliminating redundant links while +/// preserving the semantics of the data structure. +/// +/// +/// The type of the link address. +/// +public class Sequences : LinksDisposableDecoratorBase where TLinkAddress : IUnsignedNumber +{ + private readonly bool _enableAutomaticCompactification; + + /// + /// + /// Initializes a new instance. + /// + /// + /// + /// + /// The underlying links storage. + /// + /// + /// + /// Enables automatic compactification (deduplication) of all sequences on dispose. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Sequences(ILinks links, bool enableAutomaticCompactification = false) : base(links) + { + _enableAutomaticCompactification = enableAutomaticCompactification; + } + + /// + /// + /// Gets a value indicating whether automatic compactification is enabled. + /// + /// + /// + public bool IsAutomaticCompactificationEnabled + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _enableAutomaticCompactification; + } + + /// + /// + /// Performs compactification (deduplication) of all sequences by merging duplicate links. + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Compact() + { + var duplicatePairs = FindDuplicateLinks(); + foreach (var (duplicate, original) in duplicatePairs) + { + _links.MergeAndDelete(duplicate, original); + } + } + + /// + /// + /// Finds duplicate links that have the same source and target. + /// + /// + /// + /// + /// A collection of duplicate pairs where each pair contains (duplicate, original). + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private System.Collections.Generic.List<(TLinkAddress duplicate, TLinkAddress original)> FindDuplicateLinks() + { + var duplicatePairs = new System.Collections.Generic.List<(TLinkAddress duplicate, TLinkAddress original)>(); + var seenLinks = new System.Collections.Generic.Dictionary<(TLinkAddress source, TLinkAddress target), TLinkAddress>(); + + var constants = _links.Constants; + var query = new Link(constants.Any, constants.Any, constants.Any); + + _links.Each(link => + { + if (link == null) + { + return constants.Continue; + } + + var linkAddress = _links.GetIndex(link); + var source = _links.GetSource(link); + var target = _links.GetTarget(link); + + // Skip point links (where source == target == linkAddress) + if (source.Equals(linkAddress) && target.Equals(linkAddress)) + { + return constants.Continue; + } + + var key = (source, target); + if (seenLinks.TryGetValue(key, out var originalLink)) + { + // Found a duplicate - the current link is a duplicate of the original + duplicatePairs.Add((linkAddress, originalLink)); + } + else + { + // First time seeing this source-target combination + seenLinks[key] = linkAddress; + } + + return constants.Continue; + }, query); + + return duplicatePairs; + } + + /// + /// + /// Disposes the sequences manager and optionally performs automatic compactification. + /// + /// + /// + /// + /// The manual disposal flag. + /// + /// + /// + /// The was disposed flag. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void Dispose(bool manual, bool wasDisposed) + { + if (!wasDisposed && _enableAutomaticCompactification) + { + try + { + Compact(); + } + catch + { + // Ignore any errors during compactification + // The underlying links storage might be in an inconsistent state + } + } + + // Don't dispose the underlying links - let the caller handle that + // base.Dispose(manual, wasDisposed); + } +} \ No newline at end of file