diff --git a/LogGrokX.Benchmarks/DataPortBenchmark.cs b/LogGrokX.Benchmarks/DataPortBenchmark.cs new file mode 100644 index 0000000..6f70d79 --- /dev/null +++ b/LogGrokX.Benchmarks/DataPortBenchmark.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using BenchmarkDotNet.Attributes; +using LogGrokX.Data; +using LogGrokX.Data.Index; +using LogGrokX.Data.IndexTree; +using LogGrokX.Data.Monikers; + +namespace LogGrokX.Benchmarks; + +[MemoryDiagnoser] +[InProcess] +public class DataPortBenchmark +{ + private CountIndex>> _countIndex = null!; + private BenchmarkIndexer _indexer = null!; + + private sealed class BenchmarkIndexer : Indexer + { + public int GetUncachedCount(int componentIndex, string componentValue) => Indices + .Where(pair => NumbersToKeys[pair.Key].GetComponent(componentIndex).SequenceEqual(componentValue.AsSpan())) + .Sum(pair => pair.Value.Count); + } + + [GlobalSetup] + public void Setup() + { + var indices = new Dictionary>>(); + for (var key = 0; key < 256; key++) + { + var tree = new IndexTree>(16, value => new SimpleLeaf(value, 0)); + for (var line = 0; line < 64; line++) + tree.Add(line); + indices.Add(new IndexKeyNum { KeyNum = key }, tree); + } + + _countIndex = new CountIndex>>(indices); + _countIndex.Add(1, indices); + + _indexer = new BenchmarkIndexer(); + var meta = new LogMetaInformation(new LogFormat + { + Regex = @"^(?'Value'\w+)$", + IndexedFields = ["Value"] + }); + var parser = new RegexBasedLineParser(meta, true); + for (var key = 0; key < 256; key++) + { + var line = $"value{key}"; + var placeholder = new int[LineMetaInformation.GetSizeInts(1)]; + var lineMeta = new LineMetaInformation(placeholder.AsSpan(), 1); + if (!parser.TryParse(line, 0, line.Length, lineMeta.ParsedLineComponents, out _)) + throw new System.InvalidOperationException(); + var metaChars = MemoryMarshal.Cast(placeholder.AsSpan()); + var chars = new char[metaChars.Length + line.Length]; + metaChars.CopyTo(chars.AsSpan()); + line.AsSpan().CopyTo(chars.AsSpan(metaChars.Length)); + _indexer.Add(new IndexKey(new string(chars), 0, 1), key); + } + } + + [Benchmark] + public int ReadLiveCounts() => _countIndex.Counts[^1].Count; + + [Benchmark] + public int ReadCachedComponentCount() => _indexer.GetIndexCountForComponent(0, "value128"); + + [Benchmark] + public int ReadUncachedComponentCount() => _indexer.GetUncachedCount(0, "value128"); + + [Benchmark] + public int AppendIndexLines() + { + var tree = new IndexTree>(16, value => new SimpleLeaf(value, 0)); + for (var line = 0; line < 100_000; line++) + tree.Add(line); + return tree.Count; + } +} diff --git a/LogGrokX.Data.Tests/ConcurrentIndexReadTests.cs b/LogGrokX.Data.Tests/ConcurrentIndexReadTests.cs new file mode 100644 index 0000000..0c6c167 --- /dev/null +++ b/LogGrokX.Data.Tests/ConcurrentIndexReadTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using LogGrokX.Data.Index; +using LogGrokX.Data.IndexTree; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace LogGrokX.Data.Tests; + +[TestClass] +public class ConcurrentIndexReadTests +{ + [TestMethod] + public void IndexTreeCanBeReadWhileGrowing() + { + const int total = 100_000; + var tree = new IndexTree>(16, value => new SimpleLeaf(value, 0)); + var ready = new CountdownEvent(2); + var done = false; + + var readers = Enumerable.Range(0, 2).Select(_ => Task.Factory.StartNew(() => + { + ready.Signal(); + while (!Volatile.Read(ref done)) + { + var count = tree.Count; + if (count == 0) + continue; + var index = count / 2; + Assert.AreEqual(index, tree[index]); + Assert.AreEqual(index, tree.FindIndexByValue(index)); + Assert.AreEqual(index, tree.GetEnumerableFromIndex(index).First()); + } + }, TaskCreationOptions.LongRunning)).ToArray(); + + ready.Wait(); + try + { + for (var i = 0; i < total; i++) + { + tree.Add(i); + if (i % 256 == 0) + Thread.Yield(); + } + } + finally + { + Volatile.Write(ref done, true); + Task.WaitAll(readers); + } + + Assert.AreEqual(total, tree.Count); + Assert.AreEqual(total - 1, tree[total - 1]); + } + + [TestMethod] + public void ChunkedListCanBeReadWhileGrowing() + { + const int total = 100_000; + var list = new ChunkedList(64); + var ready = new CountdownEvent(2); + var done = false; + + var readers = Enumerable.Range(0, 2).Select(_ => Task.Factory.StartNew(() => + { + ready.Signal(); + while (!Volatile.Read(ref done)) + { + var count = list.Count; + if (count > 0) + Assert.AreEqual(count - 1, list[count - 1]); + } + }, TaskCreationOptions.LongRunning)).ToArray(); + + ready.Wait(); + try + { + for (var i = 0; i < total; i++) + { + list.Add(i); + if (i % 256 == 0) + Thread.Yield(); + } + } + finally + { + Volatile.Write(ref done, true); + Task.WaitAll(readers); + } + + Assert.AreEqual(total, list.Count); + Assert.AreEqual(total - 1, list[total - 1]); + } + + [TestMethod] + public void LongsLeafFindAndEnumeratePreserveOffset() + { + var tree = new IndexTree(16, value => new LongsLeaf(value, 0)); + for (var i = 0; i < 70_000; i++) + tree.Add(1_000_000L + i); + + Assert.AreEqual(65_536, tree.FindIndexByValue(1_065_536)); + Assert.AreEqual(1_065_536L, tree.GetEnumerableFromIndex(65_536).First()); + } +} diff --git a/LogGrokX.Data.Tests/CountIndexCacheTests.cs b/LogGrokX.Data.Tests/CountIndexCacheTests.cs new file mode 100644 index 0000000..5c7074b --- /dev/null +++ b/LogGrokX.Data.Tests/CountIndexCacheTests.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using LogGrokX.Data.Index; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace LogGrokX.Data.Tests; + +[TestClass] +public class CountIndexCacheTests +{ + [TestMethod] + public void CountsReuseSnapshotUntilNextLineAndFinish() + { + var key = new IndexKeyNum { KeyNum = 1 }; + var index = new LogGrokX.Data.Index.Index(4); + var indices = new Dictionary { [key] = index }; + var counts = new CountIndex(indices); + + index.Add(0); + counts.Add(0, indices); + var first = counts.Counts; + Assert.AreSame(first, counts.Counts); + Assert.AreEqual(1, first[^1][0].Item2); + + index.Add(1); + counts.Add(1, indices); + var second = counts.Counts; + Assert.AreNotSame(first, second); + Assert.AreEqual(2, second[^1][0].Item2); + + counts.Finish(indices); + var finished = counts.Counts; + Assert.AreSame(finished, counts.Counts); + Assert.AreEqual(2, finished[^1][0].Item2); + } +} diff --git a/LogGrokX.Data.Tests/IndexTreeEdgeTests.cs b/LogGrokX.Data.Tests/IndexTreeEdgeTests.cs index db391b9..46b54d6 100644 --- a/LogGrokX.Data.Tests/IndexTreeEdgeTests.cs +++ b/LogGrokX.Data.Tests/IndexTreeEdgeTests.cs @@ -23,6 +23,16 @@ public void LongsLeafFindByValue() Assert.AreEqual(2, missingIndex); } + [TestMethod] + public void LongsLeafUsesFirstValueWhenIndexDiffers() + { + var leaf = new LongsLeaf(1_000_000, 5); + leaf.Add(1_000_004, 6); + + Assert.AreEqual(6, leaf.FindByValue(1_000_004).index); + CollectionAssert.AreEqual(new[] { 1_000_000L, 1_000_004L }, new List(leaf)); + } + [TestMethod] public void LongsLeafChainsWhenFull() { diff --git a/LogGrokX.Data.Tests/IndexerTests.cs b/LogGrokX.Data.Tests/IndexerTests.cs index b08a3f8..fd7d1be 100644 --- a/LogGrokX.Data.Tests/IndexerTests.cs +++ b/LogGrokX.Data.Tests/IndexerTests.cs @@ -77,6 +77,24 @@ public void GetIndexCountForComponentCountsLines() Assert.AreEqual(0, indexer.GetIndexCountForComponent(0, "missing")); } + [TestMethod] + public void GetIndexCountForComponentRefreshesCachedKeysAndLiveCounts() + { + using var indexer = new Indexer(); + indexer.Add(Key("alpha beta one"), 0); + Assert.AreEqual(1, indexer.GetIndexCountForComponent(0, "alpha")); + + indexer.Add(Key("alpha beta one"), 1); + Assert.AreEqual(2, indexer.GetIndexCountForComponent(0, "alpha")); + + indexer.Add(Key("gamma delta two"), 2); + Assert.AreEqual(2, indexer.GetIndexCountForComponent(0, "alpha")); + + indexer.Add(Key("alpha beta three"), 3); + Assert.AreEqual(3, indexer.GetIndexCountForComponent(0, "alpha")); + Assert.AreEqual(1, indexer.GetIndexCountForComponent(0, "gamma")); + } + [TestMethod] public void GetIndexKeyNumAndIndexRoundTrip() { diff --git a/LogGrokX.Data.Tests/LineIndexEdgeTests.cs b/LogGrokX.Data.Tests/LineIndexEdgeTests.cs index a3baab1..4fb8ebb 100644 --- a/LogGrokX.Data.Tests/LineIndexEdgeTests.cs +++ b/LogGrokX.Data.Tests/LineIndexEdgeTests.cs @@ -52,4 +52,35 @@ public async Task FetchRangesRefreshesCountAfterDelay() CollectionAssert.AreEqual(new[] { (0, 1) }, ranges); } + + [TestMethod] + public async Task FetchRangesWakesWhenEnoughLinesArrive() + { + var index = new LineIndex(); + using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await using var ranges = index.FetchRanges(cancellation.Token).GetAsyncEnumerator(); + var next = ranges.MoveNextAsync().AsTask(); + + for (var i = 0; i <= 256; i++) + index.Add(i * 10L); + + Assert.IsTrue(await next.WaitAsync(TimeSpan.FromMilliseconds(200))); + Assert.AreEqual((0, 256), ranges.Current); + index.Finish(10); + } + + [TestMethod] + public async Task FetchRangesWakesWhenFinishedBelowBatchSize() + { + var index = new LineIndex(); + index.Add(0); + using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await using var ranges = index.FetchRanges(cancellation.Token).GetAsyncEnumerator(); + var next = ranges.MoveNextAsync().AsTask(); + + index.Finish(10); + + Assert.IsTrue(await next.WaitAsync(TimeSpan.FromMilliseconds(200))); + Assert.AreEqual((0, 1), ranges.Current); + } } diff --git a/LogGrokX.Data.Tests/LineProviderRangeTests.cs b/LogGrokX.Data.Tests/LineProviderRangeTests.cs new file mode 100644 index 0000000..274e21f --- /dev/null +++ b/LogGrokX.Data.Tests/LineProviderRangeTests.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using LogGrokX.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace LogGrokX.Data.Tests; + +[TestClass] +public class LineProviderRangeTests +{ + private sealed class HugeRangeLineIndex : ILineIndex + { + public int Count => 2; + public (long offset, int length) GetLine(int index) => + index == 0 ? (0, 0) : (3_000_000_000L, 10); + public void Fetch(int start, Span<(long offset, int length)> values) => + throw new InvalidOperationException("Unexpected read"); + } + + [TestMethod] + public void FetchRejectsRangeLargerThanInt32() + { + var path = Path.GetTempFileName(); + try + { + File.WriteAllText(path, "sample"); + var provider = new LineProvider(new HugeRangeLineIndex(), new LogFile(path, 0)); + Assert.ThrowsExactly(() => provider.Fetch(0, new (int, string)[2])); + } + finally + { + File.Delete(path); + } + } +} diff --git a/LogGrokX.Data.Tests/StringPoolLimitTests.cs b/LogGrokX.Data.Tests/StringPoolLimitTests.cs new file mode 100644 index 0000000..d1ecc87 --- /dev/null +++ b/LogGrokX.Data.Tests/StringPoolLimitTests.cs @@ -0,0 +1,22 @@ +using LogGrokX.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace LogGrokX.Data.Tests; + +[TestClass] +public class StringPoolLimitTests +{ + [TestMethod] + public void ReturnCapsEachBucketAndRentDecrementsCount() + { + var pool = new StringPool(); + _ = pool.Rent(50); + for (var i = 0; i < 200; i++) + pool.Return(new string('\0', 64)); + + Assert.AreEqual(64, pool.GetPooledCount(50)); + _ = pool.Rent(50); + Assert.AreEqual(63, pool.GetPooledCount(50)); + Assert.AreEqual(0, pool.GetPooledCount(100)); + } +} diff --git a/LogGrokX.Data/Index/ChunkedList.cs b/LogGrokX.Data/Index/ChunkedList.cs index c0d7884..1213da0 100644 --- a/LogGrokX.Data/Index/ChunkedList.cs +++ b/LogGrokX.Data/Index/ChunkedList.cs @@ -1,7 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; +using System.Linq; +using System.Threading; namespace LogGrokX.Data.Index; @@ -18,16 +19,17 @@ public ChunkedList(int chunkSize) private IEnumerable GetEnumerableFrom(int index) { - if (index >= _count) + var count = Volatile.Read(ref _count); + if (index >= count) throw new IndexOutOfRangeException(); var chunkNum = index / _chunkSize; var from = index % _chunkSize; - while (index < _count) + while (index < count) { var currentChunk = _chunks[chunkNum]; - var to = Math.Min(_count - chunkNum * _chunkSize, _chunkSize); + var to = Math.Min(count - chunkNum * _chunkSize, _chunkSize); for (var idx = from; idx < to; idx++) yield return currentChunk[idx]; @@ -51,13 +53,13 @@ public void Add(T item) var lastChunk = _chunks[^1]; lastChunk[idx] = item; - _count++; + Volatile.Write(ref _count, _count + 1); } public void Clear() { _chunks.Clear(); - _count = 0; + Volatile.Write(ref _count, 0); } public bool Contains(T item) => throw new NotSupportedException(); @@ -66,7 +68,7 @@ public void Clear() public bool Remove(T item) => throw new NotSupportedException(); - public int Count => _count; + public int Count => Volatile.Read(ref _count); public bool IsReadOnly => false; @@ -80,7 +82,7 @@ public T this[int index] { get { - if (index >= _count) + if (index >= Volatile.Read(ref _count)) throw new IndexOutOfRangeException(); return _chunks[index / _chunkSize][index % _chunkSize]; @@ -88,4 +90,4 @@ public T this[int index] set => throw new NotSupportedException(); } -} \ No newline at end of file +} diff --git a/LogGrokX.Data/Index/CountIndex.cs b/LogGrokX.Data/Index/CountIndex.cs index 04485ff..6158b8f 100644 --- a/LogGrokX.Data/Index/CountIndex.cs +++ b/LogGrokX.Data/Index/CountIndex.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; -using System.Collections.Immutable; +using System.Collections.Immutable; +using System.Threading; namespace LogGrokX.Data.Index { @@ -8,16 +9,31 @@ public class CountIndex where TIndex : IIndex public const int Granularity = 16384; private ImmutableList> _counts = ImmutableList>.Empty; private readonly IDictionary _indices; - private bool _isFinished = false; + private volatile bool _isFinished; + private int _lastIndex = -1; + private LiveSnapshot? _liveSnapshot; + + private sealed class LiveSnapshot(int index, IReadOnlyList> counts) + { + public int Index { get; } = index; + public IReadOnlyList> Counts { get; } = counts; + } public IReadOnlyList> Counts { get { - if (_isFinished) - return _counts; - var counts = _counts.Add(MakeCountsSnapshot()); - return _isFinished ? _counts : counts; + if (_isFinished) + return _counts; + var index = Volatile.Read(ref _lastIndex); + var cached = Volatile.Read(ref _liveSnapshot); + if (cached != null && cached.Index == index) + return cached.Counts; + var counts = _counts.Add(MakeCountsSnapshot()); + if (_isFinished) + return _counts; + Volatile.Write(ref _liveSnapshot, new LiveSnapshot(index, counts)); + return counts; } } @@ -28,8 +44,9 @@ public CountIndex(IDictionary indices) public void Add(int currentIndex, IDictionary indices) { - if (currentIndex % Granularity == 0 && currentIndex != 0) - UpdateCountsSnapshot(); + if (currentIndex % Granularity == 0 && currentIndex != 0) + UpdateCountsSnapshot(); + Volatile.Write(ref _lastIndex, currentIndex); } public void Finish(IDictionary indices) @@ -58,4 +75,4 @@ private void UpdateCountsSnapshot() return snapshotList; } } -} \ No newline at end of file +} diff --git a/LogGrokX.Data/Index/IndexerBase.cs b/LogGrokX.Data/Index/IndexerBase.cs index 8324790..f078e89 100644 --- a/LogGrokX.Data/Index/IndexerBase.cs +++ b/LogGrokX.Data/Index/IndexerBase.cs @@ -14,7 +14,9 @@ public abstract class IndexerBase : IDisposable protected readonly CountIndex>> CountIndex; protected readonly ConcurrentDictionary KeysToNumbers; - protected readonly ConcurrentDictionary NumbersToKeys; + protected readonly ConcurrentDictionary NumbersToKeys; + private readonly ConcurrentDictionary<(int componentIndex, string value), (int keyCount, IndexKeyNum[] keys)> + _componentKeyCache = new(); public IndexerBase(ConcurrentDictionary keysToNumbers, ConcurrentDictionary numbersToKeys) @@ -38,12 +40,26 @@ public IIndexedLinesProvider GetIndexedLinesProvider( public IndexTree> GetIndex(IndexKeyNum key) => Indices[key]; - public int GetIndexCountForComponent(int componentIndex, string componentValue) - { - return Indices - .Where(keyValuePair => - NumbersToKeys[keyValuePair.Key].GetComponent(componentIndex).SequenceEqual(componentValue.AsSpan())) - .Sum(kv => kv.Value.Count); + public int GetIndexCountForComponent(int componentIndex, string componentValue) + { + var keyCount = Indices.Count; + var cacheKey = (componentIndex, componentValue); + if (!_componentKeyCache.TryGetValue(cacheKey, out var cached) || cached.keyCount != keyCount) + { + var keys = Indices.Keys + .Where(key => NumbersToKeys[key].GetComponent(componentIndex).SequenceEqual(componentValue.AsSpan())) + .ToArray(); + cached = (keyCount, keys); + _componentKeyCache[cacheKey] = cached; + } + + var sum = 0; + foreach (var key in cached.keys) + { + if (Indices.TryGetValue(key, out var index)) + sum += index.Count; + } + return sum; } private protected static IndexTree> CreateIndexTree() @@ -54,11 +70,12 @@ private protected static IndexTree> CreateIndexTree() public void Dispose() { - Indices.Clear(); + Indices.Clear(); + _componentKeyCache.Clear(); } public void Finish() { CountIndex.Finish(Indices); } -} \ No newline at end of file +} diff --git a/LogGrokX.Data/IndexTree/IndexTree.cs b/LogGrokX.Data/IndexTree/IndexTree.cs index 8c70285..3df54e6 100644 --- a/LogGrokX.Data/IndexTree/IndexTree.cs +++ b/LogGrokX.Data/IndexTree/IndexTree.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; +using System.Linq; +using System.Threading; using LogGrokX.Data.Index; namespace LogGrokX.Data.IndexTree @@ -34,26 +35,28 @@ public void Add(T value) else { _currentLeaf = _createFirstLeaf(value); - _head = _currentLeaf; - } - _count++; + Volatile.Write(ref _head, _currentLeaf); + } + Volatile.Write(ref _count, _count + 1); } - public int Count => _count; + public int Count => Volatile.Read(ref _count); public IEnumerable GetEnumerableFromIndex(int index) { - return _head == null ? Enumerable.Empty() : _head.GetEnumerableFromIndex(index); + var head = Volatile.Read(ref _head); + return head == null ? Enumerable.Empty() : head.GetEnumerableFromIndex(index); } public IEnumerable GetEnumerableFromValue(T value) { - return _head == null ? Enumerable.Empty() : _head.GetEnumerableFromValue(value); + var head = Volatile.Read(ref _head); + return head == null ? Enumerable.Empty() : head.GetEnumerableFromValue(value); } public int FindIndexByValue(T value) { - return _head?.GetIndexByValue(value) ?? 0; + return Volatile.Read(ref _head)?.GetIndexByValue(value) ?? 0; } private void OnNewLeafCreated(TLeaf newLeaf) @@ -63,11 +66,11 @@ private void OnNewLeafCreated(TLeaf newLeaf) case TreeNode headNode: var newNode = AddToTree(headNode, newLeaf); if (newNode != null) - _head = new TreeNode(_nodeCapacity, _head, newNode); + Volatile.Write(ref _head, new TreeNode(_nodeCapacity, _head, newNode)); break; case TLeaf leaf: var treeHead = new TreeNode(_nodeCapacity, leaf, newLeaf); - _head = treeHead; + Volatile.Write(ref _head, treeHead); break; } _currentLeaf = newLeaf; @@ -93,11 +96,12 @@ public T this[int idx] { get { - Debug.Assert(idx <= _count); - if (_head == null) - throw new InvalidOperationException("Unable to get element from empty tree."); - return _head.GetValue(idx); + Debug.Assert(idx <= Volatile.Read(ref _count)); + var head = Volatile.Read(ref _head); + if (head == null) + throw new InvalidOperationException("Unable to get element from empty tree."); + return head.GetValue(idx); } } } -} \ No newline at end of file +} diff --git a/LogGrokX.Data/IndexTree/LongsLeaf.cs b/LogGrokX.Data/IndexTree/LongsLeaf.cs index d75f23c..25b4f36 100644 --- a/LogGrokX.Data/IndexTree/LongsLeaf.cs +++ b/LogGrokX.Data/IndexTree/LongsLeaf.cs @@ -1,5 +1,6 @@ using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Threading; namespace LogGrokX.Data.IndexTree { @@ -8,34 +9,38 @@ public sealed class LongsLeaf ILeaf { private const int Capacity = 64*1024; - private const int InitialCapacity = 1024; private readonly long _firstValue; private readonly int _firstIndex; - private readonly List _storage; + private readonly List _storage; + private int _count; + private LongsLeaf? _next; public LongsLeaf(long firstValue, int valueIndex) { - _storage = new List(InitialCapacity) {0}; - _firstIndex = valueIndex; - _firstValue = firstValue; + _storage = new List(Capacity) {0}; + _firstIndex = valueIndex; + _firstValue = firstValue; + Volatile.Write(ref _count, 1); } public LongsLeaf? Add(long value, int valueIndex) { if (_storage.Count < Capacity) { - _storage.Add((int)(value - _firstValue)); - return null; + _storage.Add((int)(value - _firstValue)); + Volatile.Write(ref _count, _storage.Count); + return null; } - Next = new LongsLeaf(value, valueIndex); - return Next; + var next = new LongsLeaf(value, valueIndex); + Volatile.Write(ref _next, next); + return next; } public long this[int index] => _firstValue +_storage[index]; - public int Count => _storage.Count; - public LongsLeaf? Next { get; private set; } + public int Count => Volatile.Read(ref _count); + public LongsLeaf? Next => Volatile.Read(ref _next); public override long FirstValue => _firstValue; public override int MinIndex => _firstIndex; @@ -52,7 +57,7 @@ public override long GetValue(int index) public override (int index, LongsLeaf leaf) FindByValue(long value) { - var index = _storage.BinarySearch((int) (value - _firstValue)); + var index = _storage.BinarySearch(0, Volatile.Read(ref _count), (int) (value - _firstValue), null); return (_firstIndex + (index >= 0 ? index : ~index), this); } @@ -69,4 +74,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } -} \ No newline at end of file +} diff --git a/LogGrokX.Data/IndexTree/SimpleLeaf.cs b/LogGrokX.Data/IndexTree/SimpleLeaf.cs index 79289f3..5157c3f 100644 --- a/LogGrokX.Data/IndexTree/SimpleLeaf.cs +++ b/LogGrokX.Data/IndexTree/SimpleLeaf.cs @@ -1,5 +1,6 @@ using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Threading; namespace LogGrokX.Data.IndexTree { @@ -8,26 +9,30 @@ public sealed class SimpleLeaf : ILeaf>, ITreeNode { private readonly List _storage; - private readonly int _firstValueIndex; - private const int LeafCapacity = 1024; - private const int InitialLeafCapacity = 16; + private readonly int _firstValueIndex; + private int _count; + private SimpleLeaf? _next; + private const int LeafCapacity = 1024; public SimpleLeaf(T firstValue, int valueIndex) { - _storage = new List(InitialLeafCapacity) {firstValue}; - _firstValueIndex = valueIndex; + _storage = new List(LeafCapacity) {firstValue}; + _firstValueIndex = valueIndex; + Volatile.Write(ref _count, 1); } public SimpleLeaf? Add(T value, int valueIndex) { if (_storage.Count < LeafCapacity) { - _storage.Add(value); - return null; + _storage.Add(value); + Volatile.Write(ref _count, _storage.Count); + return null; } - Next = new SimpleLeaf(value, valueIndex); - return Next; + var next = new SimpleLeaf(value, valueIndex); + Volatile.Write(ref _next, next); + return next; } public override T FirstValue => _storage[0]; @@ -36,7 +41,7 @@ public SimpleLeaf(T firstValue, int valueIndex) public T this[int index] => _storage[index]; - public int Count => _storage.Count; + public int Count => Volatile.Read(ref _count); public override IEnumerable GetEnumerableFromIndex(int index) { @@ -50,11 +55,11 @@ public override T GetValue(int index) public override (int index, SimpleLeaf leaf) FindByValue(T value) { - var index = _storage.BinarySearch(value); + var index = _storage.BinarySearch(0, Volatile.Read(ref _count), value, null); return ((index >= 0 ? index : ~index) + _firstValueIndex, this); } - public SimpleLeaf? Next { get; private set; } + public SimpleLeaf? Next => Volatile.Read(ref _next); public IEnumerator GetEnumerator() { @@ -66,4 +71,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } -} \ No newline at end of file +} diff --git a/LogGrokX.Data/IndexTree/TreeNode.cs b/LogGrokX.Data/IndexTree/TreeNode.cs index 9e0916b..5338722 100644 --- a/LogGrokX.Data/IndexTree/TreeNode.cs +++ b/LogGrokX.Data/IndexTree/TreeNode.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; +using System.Threading; using LogGrokX.Data.Index; namespace LogGrokX.Data.IndexTree @@ -9,26 +10,32 @@ public class TreeNode : LeafOrNode where TLeaf : class, ILeaf, ITreeNode, IEnumerable where T : IComparable { - private readonly List> _subNodes; + private readonly List> _subNodes; + private int _count; public TreeNode(int nodeCapacity, LeafOrNode first, LeafOrNode second) { Debug.Assert(nodeCapacity > 1); - _subNodes = new List>(nodeCapacity) {first, second}; + _subNodes = new List>(nodeCapacity) {first, second}; + Volatile.Write(ref _count, 2); } private TreeNode(int nodeCapacity, LeafOrNode first) { Debug.Assert(nodeCapacity > 1); - _subNodes = new List>(nodeCapacity) {first}; + _subNodes = new List>(nodeCapacity) {first}; + Volatile.Write(ref _count, 1); } internal LeafOrNode LastSubNode => _subNodes[^1]; public TreeNode? TryAdd(LeafOrNode node) { - if (_subNodes.Count < _subNodes.Capacity) - _subNodes.Add(node); + if (_subNodes.Count < _subNodes.Capacity) + { + _subNodes.Add(node); + Volatile.Write(ref _count, _subNodes.Count); + } else return new TreeNode(_subNodes.Capacity, node); @@ -42,7 +49,7 @@ public override IEnumerable GetEnumerableFromIndex(int index) => public override (int index, TLeaf leaf) FindByValue(T value) { - var index = _subNodes.BinarySearch(0, _subNodes.Count, value, + var index = _subNodes.BinarySearch(0, Volatile.Read(ref _count), value, static (leafOrNode, t) => leafOrNode.FirstValue.CompareTo(t)); var subNodeIndex = index >= 0 ? index : ~index - 1; return _subNodes[subNodeIndex].FindByValue(value); @@ -50,7 +57,7 @@ public override (int index, TLeaf leaf) FindByValue(T value) private LeafOrNode GetSubNodeByIndex(int index) { - var found = _subNodes.BinarySearch(0, _subNodes.Count, index, + var found = _subNodes.BinarySearch(0, Volatile.Read(ref _count), index, static (leafOrNode, t) => leafOrNode.MinIndex.CompareTo(t)); // When i is < 0, ~i is index of the first element, which MinIndex is greater than index. @@ -62,4 +69,4 @@ private LeafOrNode GetSubNodeByIndex(int index) public override T FirstValue => _subNodes[0].FirstValue; public override int MinIndex => _subNodes[0].MinIndex; } -} \ No newline at end of file +} diff --git a/LogGrokX.Data/LineIndex.cs b/LogGrokX.Data/LineIndex.cs index cc138cf..c2ab15f 100644 --- a/LogGrokX.Data/LineIndex.cs +++ b/LogGrokX.Data/LineIndex.cs @@ -68,11 +68,16 @@ public void Fetch(int start, Span<(long offset, int length)> values) { while (currentIndex + minRangeSize > currentCount && !IsFinished) { - await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken); - currentCount = Count; + var changed = _dataChangedSignal.Task; + currentCount = Count; + if (currentIndex + minRangeSize <= currentCount || IsFinished) + break; + await changed.WaitAsync(cancellationToken); + ResetDataChangedSignal(); + currentCount = Count; } } - catch (TaskCanceledException) + catch (OperationCanceledException) { yield break; } @@ -86,7 +91,20 @@ public void Fetch(int start, Span<(long offset, int length)> values) currentIndex += rangeSize; currentCount = Count; } - } + } + + private volatile TaskCompletionSource _dataChangedSignal = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + private void NotifyDataChanged() => _dataChangedSignal.TrySetResult(); + + private void ResetDataChangedSignal() + { + var current = _dataChangedSignal; + if (current.Task.IsCompleted) + _ = Interlocked.CompareExchange(ref _dataChangedSignal, + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously), current); + } public int Count { @@ -102,22 +120,33 @@ public int Count } } - public int Add(long lineStart) - { - lock (_lineStarts) - { - var lineNum = _lineStarts.Count; - _lineStarts.Add(lineStart); - return lineNum; - } + public int Add(long lineStart) + { + int lineNum; + lock (_lineStarts) + { + lineNum = _lineStarts.Count; + _lineStarts.Add(lineStart); + } + NotifyDataChanged(); + return lineNum; } public void Finish(int lastLength) { - _lastLineLength = lastLength; + lock (_lineStarts) + _lastLineLength = lastLength; + NotifyDataChanged(); } - public bool IsFinished => _lastLineLength.HasValue; + public bool IsFinished + { + get + { + lock (_lineStarts) + return _lastLineLength.HasValue; + } + } private readonly IndexTree _lineStarts = new(16, l => new LongsLeaf(l, 0)); diff --git a/LogGrokX.Data/LineProvider.cs b/LogGrokX.Data/LineProvider.cs index 2b4043e..bbe6bc3 100644 --- a/LogGrokX.Data/LineProvider.cs +++ b/LogGrokX.Data/LineProvider.cs @@ -27,7 +27,11 @@ public void Fetch(int start, Span<(int, string)> values) var count = values.Length; var (lastLineOffset, lastLineLength) = _lineIndex.GetLine(start + count - 1); - var size = (int)(lastLineOffset + lastLineLength - startOffset); + var sizeLong = lastLineOffset + lastLineLength - startOffset; + if (sizeLong is < 0 or > int.MaxValue) + throw new InvalidOperationException( + $"Cannot fetch lines [{start}..{start + count - 1}]: byte range {sizeLong} is out of Int32 range."); + var size = (int)sizeLong; using var owner = MemoryPool.Shared.Rent(size); @@ -49,4 +53,4 @@ public void Fetch(int start, Span<(int, string)> values) } } } -} \ No newline at end of file +} diff --git a/LogGrokX.Data/StringPool.cs b/LogGrokX.Data/StringPool.cs index 277dca7..8fc2142 100644 --- a/LogGrokX.Data/StringPool.cs +++ b/LogGrokX.Data/StringPool.cs @@ -1,30 +1,43 @@ using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; +using System.Threading; namespace LogGrokX.Data { - public class StringPool - { - private class StringPoolBucket + public class StringPool + { + private const int MaxPooledPerBucket = 64; + private class StringPoolBucket { private readonly int _stringSize; - private readonly ConcurrentBag _pool = new(); + private readonly ConcurrentBag _pool = new(); + private int _pooledCount; public StringPoolBucket(int stringSize) { _stringSize = stringSize; } public string Rent() { - if (_pool.TryTake(out var result)) - return result; + if (_pool.TryTake(out var result)) + { + Interlocked.Decrement(ref _pooledCount); + return result; + } return new string('\0', _stringSize); } - public void Return(string returned) - { - _pool.Add(returned); - } + public void Return(string returned) + { + if (Interlocked.Increment(ref _pooledCount) > MaxPooledPerBucket) + { + Interlocked.Decrement(ref _pooledCount); + return; + } + _pool.Add(returned); + } + + public int PooledCount => Volatile.Read(ref _pooledCount); } ConcurrentDictionary _buckets = new(); @@ -37,13 +50,19 @@ public string Rent(int size) return bucket.Rent(); } - public void Return(string returned) + public void Return(string returned) { if (!_buckets.TryGetValue(returned.Length, out var bucket)) throw new InvalidOperationException(); - bucket.Return(returned); - } + bucket.Return(returned); + } + + internal int GetPooledCount(int size) + { + var pooledStringSize = size < 32 ? 32 : Pow2Roundup(size); + return _buckets.TryGetValue(pooledStringSize, out var bucket) ? bucket.PooledCount : 0; + } private static int Pow2Roundup (int x) { @@ -56,4 +75,4 @@ private static int Pow2Roundup (int x) return x+1; } } -} \ No newline at end of file +}