-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Improve
]: scan operations for IReadOnlyStoreView
and fix of Find
with all 0xff keyPrefix
#3688
base: master
Are you sure you want to change the base?
Changes from all commits
3b7cd98
6ccb8ca
5628012
a81b8f6
16c9ef2
86a8d75
77fb843
7477f87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// Copyright (C) 2015-2025 The Neo Project. | ||
// | ||
// ArrayExtensions.cs file belongs to the neo project and is free | ||
// software distributed under the MIT software license, see the | ||
// accompanying file LICENSE in the main directory of the | ||
// repository or http://www.opensource.org/licenses/mit-license.php | ||
// for more details. | ||
// | ||
// Redistribution and use in source and binary forms with or without | ||
// modifications are permitted. | ||
|
||
using System; | ||
using System.Runtime.CompilerServices; | ||
|
||
namespace Neo.Extensions | ||
{ | ||
public static class ArrayExtensions | ||
{ | ||
/// <summary> | ||
/// Creates an array of the specified length, filled with the specified value. | ||
/// </summary> | ||
/// <typeparam name="T">The type of the array elements.</typeparam> | ||
/// <param name="value">The value to fill the array with.</param> | ||
/// <param name="count">The number of elements in the array.</param> | ||
/// <returns>An array of the specified length, filled with the specified value.</returns> | ||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public static T[] Repeat<T>(this T value, int count) | ||
{ | ||
T[] array = new T[count]; | ||
Array.Fill(array, value); | ||
return array; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,157 @@ | ||||||
// Copyright (C) 2015-2025 The Neo Project. | ||||||
// | ||||||
// ReadOnlyViewExtensions.cs file belongs to the neo project and is free | ||||||
// software distributed under the MIT software license, see the | ||||||
// accompanying file LICENSE in the main directory of the | ||||||
// repository or http://www.opensource.org/licenses/mit-license.php | ||||||
// for more details. | ||||||
// | ||||||
// Redistribution and use in source and binary forms with or without | ||||||
// modifications are permitted. | ||||||
|
||||||
#nullable enable | ||||||
|
||||||
using Neo.Persistence; | ||||||
using Neo.SmartContract; | ||||||
using System; | ||||||
using System.Collections.Generic; | ||||||
using System.Linq; | ||||||
using System.Runtime.CompilerServices; | ||||||
|
||||||
namespace Neo.Extensions | ||||||
{ | ||||||
public static class ReadOnlyViewExtensions | ||||||
{ | ||||||
/// <summary> | ||||||
/// Scans the entries starting with the specified prefix. | ||||||
/// <para> | ||||||
/// If <paramref name="direction"/> is <see cref="SeekDirection.Forward"/>, | ||||||
/// it seeks to the first entry if <paramref name="keyPrefix"/> is null or empty. | ||||||
/// </para> | ||||||
/// <para> | ||||||
/// If <paramref name="direction"/> is <see cref="SeekDirection.Backward"/>, | ||||||
/// the <paramref name="keyPrefix"/> cannot be null or empty. | ||||||
/// </para> | ||||||
/// <para> | ||||||
/// If want to scan all entries with <see cref="SeekDirection.Backward"/>, | ||||||
/// set <paramref name="keyPrefix"/> to be N * 0xff, where N is the max length of the key. | ||||||
/// See <see cref="ArrayExtensions.Repeat"/>. | ||||||
/// </para> | ||||||
/// </summary> | ||||||
/// <param name="view">The view to scan.</param> | ||||||
/// <param name="keyPrefix">The prefix of the key.</param> | ||||||
/// <param name="direction">The search direction.</param> | ||||||
/// <returns>The entries found with the desired prefix.</returns> | ||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||||
public static IEnumerable<(StorageKey Key, StorageItem Value)> ScanPrefix( | ||||||
this IReadOnlyStoreView view, | ||||||
byte[]? keyPrefix, | ||||||
SeekDirection direction = SeekDirection.Forward) | ||||||
{ | ||||||
var seekPrefix = direction == SeekDirection.Forward ? keyPrefix : keyPrefix.GetSeekPrefix(); | ||||||
return view.ScanPrefix(keyPrefix, seekPrefix, direction); | ||||||
} | ||||||
|
||||||
internal static IEnumerable<(StorageKey Key, StorageItem Value)> ScanPrefix( | ||||||
this IReadOnlyStoreView view, | ||||||
byte[]? keyPrefix, | ||||||
byte[]? seekPrefix, | ||||||
SeekDirection direction = SeekDirection.Forward) | ||||||
{ | ||||||
foreach (var (key, value) in view.Seek(seekPrefix, direction)) | ||||||
{ | ||||||
if (keyPrefix == null || key.ToArray().AsSpan().StartsWith(keyPrefix)) | ||||||
yield return (key, value); | ||||||
else if (direction == SeekDirection.Forward || (seekPrefix == null || !key.ToArray().SequenceEqual(seekPrefix))) | ||||||
yield break; | ||||||
} | ||||||
} | ||||||
|
||||||
/// <summary> | ||||||
/// Scans the entries in the specified range. | ||||||
/// <para> | ||||||
/// If <paramref name="direction"/> is <see cref="SeekDirection.Forward"/>, | ||||||
/// it seeks to the first entry if <paramref name="inclusiveStartKey"/> is null or empty. | ||||||
/// </para> | ||||||
/// <para> | ||||||
/// If <paramref name="direction"/> is <see cref="SeekDirection.Backward"/>, | ||||||
/// the <paramref name="inclusiveStartKey"/> cannot be null or empty. | ||||||
/// </para> | ||||||
/// <para> | ||||||
/// If want to scan all entries with <see cref="SeekDirection.Backward"/>, | ||||||
/// set <paramref name="inclusiveStartKey"/> to be N * 0xff and <paramref name="exclusiveEndKey"/> to be empty, | ||||||
/// where N is the max length of the key. | ||||||
/// </para> | ||||||
/// </summary> | ||||||
/// <param name="view">The view to scan.</param> | ||||||
/// <param name="inclusiveStartKey">The inclusive start key.</param> | ||||||
/// <param name="exclusiveEndKey">The exclusive end key.</param> | ||||||
/// <param name="direction">The search direction.</param> | ||||||
/// <returns>The entries found in the specified range.</returns> | ||||||
public static IEnumerable<(StorageKey Key, StorageItem Value)> ScanRange( | ||||||
this IReadOnlyStoreView view, | ||||||
byte[]? inclusiveStartKey, | ||||||
byte[] exclusiveEndKey, | ||||||
SeekDirection direction = SeekDirection.Forward) | ||||||
{ | ||||||
ByteArrayComparer comparer = direction == SeekDirection.Forward | ||||||
? ByteArrayComparer.Default | ||||||
: ByteArrayComparer.Reverse; | ||||||
foreach (var (key, value) in view.Seek(inclusiveStartKey, direction)) | ||||||
{ | ||||||
if (comparer.Compare(key.ToArray(), exclusiveEndKey) < 0) | ||||||
yield return (key, value); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
else | ||||||
yield break; | ||||||
} | ||||||
} | ||||||
|
||||||
/// <summary> | ||||||
/// Gets the seek prefix for the specified key prefix. | ||||||
/// <para> | ||||||
/// If the <paramref name="keyPrefix"/> is all 0xff, and <paramref name="maxSizeWhenAll0xff"/> > 0, | ||||||
/// the seek prefix will be set to be byte[maxSizeWhenAll0xff] and filled with 0xff. | ||||||
/// </para> | ||||||
/// <para> | ||||||
/// If the <paramref name="keyPrefix"/> is all 0xff and <paramref name="maxSizeWhenAll0xff"/> is less than or equal to 0, | ||||||
/// an ArgumentException will be thrown. | ||||||
/// </para> | ||||||
/// </summary> | ||||||
/// <param name="keyPrefix">The key prefix.</param> | ||||||
/// <param name="maxSizeWhenAll0xff">The maximum size when all bytes are 0xff.</param> | ||||||
/// <returns>The seek prefix.</returns> | ||||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="keyPrefix"/> is null.</exception> | ||||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="keyPrefix"/> is empty.</exception> | ||||||
/// <exception cref="ArgumentException"> | ||||||
/// Thrown when <paramref name="keyPrefix"/> is all 0xff and <paramref name="maxSizeWhenAll0xff"/> is less than or equal to 0. | ||||||
/// </exception> | ||||||
internal static byte[] GetSeekPrefix(this byte[]? keyPrefix, int maxSizeWhenAll0xff = 4096 /* make it long enough */) | ||||||
{ | ||||||
if (keyPrefix == null) // Backwards seek for null prefix is not supported for now. | ||||||
throw new ArgumentNullException(nameof(keyPrefix)); | ||||||
|
||||||
if (keyPrefix.Length == 0) // Backwards seek for zero prefix is not supported for now. | ||||||
throw new ArgumentOutOfRangeException(nameof(keyPrefix)); | ||||||
|
||||||
byte[]? seekPrefix = null; | ||||||
for (var i = keyPrefix.Length - 1; i >= 0; i--) | ||||||
{ | ||||||
if (keyPrefix[i] < 0xff) | ||||||
{ | ||||||
seekPrefix = keyPrefix.Take(i + 1).ToArray(); | ||||||
seekPrefix[i]++; // The next key after the key_prefix. | ||||||
break; | ||||||
} | ||||||
} | ||||||
|
||||||
if (seekPrefix == null) | ||||||
{ | ||||||
if (maxSizeWhenAll0xff > 0) | ||||||
seekPrefix = ((byte)0xff).Repeat(maxSizeWhenAll0xff); | ||||||
else | ||||||
throw new ArgumentException($"{nameof(keyPrefix)} with all bytes being 0xff is not supported now"); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
return seekPrefix; | ||||||
} | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -202,50 +202,14 @@ public void Delete(StorageKey key) | |||||
/// <summary> | ||||||
/// Finds the entries starting with the specified prefix. | ||||||
/// </summary> | ||||||
/// <param name="key_prefix">The prefix of the key.</param> | ||||||
/// <param name="keyPrefix">The prefix of the key.</param> | ||||||
/// <param name="direction">The search direction.</param> | ||||||
/// <returns>The entries found with the desired prefix.</returns> | ||||||
public IEnumerable<(StorageKey Key, StorageItem Value)> Find(byte[]? key_prefix = null, SeekDirection direction = SeekDirection.Forward) | ||||||
public IEnumerable<(StorageKey Key, StorageItem Value)> Find(byte[]? keyPrefix = null, SeekDirection direction = SeekDirection.Forward) | ||||||
{ | ||||||
var seek_prefix = key_prefix; | ||||||
if (direction == SeekDirection.Backward) | ||||||
{ | ||||||
if (key_prefix == null) | ||||||
{ | ||||||
// Backwards seek for null prefix is not supported for now. | ||||||
throw new ArgumentNullException(nameof(key_prefix)); | ||||||
} | ||||||
if (key_prefix.Length == 0) | ||||||
{ | ||||||
// Backwards seek for zero prefix is not supported for now. | ||||||
throw new ArgumentOutOfRangeException(nameof(key_prefix)); | ||||||
} | ||||||
seek_prefix = null; | ||||||
for (var i = key_prefix.Length - 1; i >= 0; i--) | ||||||
{ | ||||||
if (key_prefix[i] < 0xff) | ||||||
{ | ||||||
seek_prefix = key_prefix.Take(i + 1).ToArray(); | ||||||
// The next key after the key_prefix. | ||||||
seek_prefix[i]++; | ||||||
break; | ||||||
} | ||||||
} | ||||||
if (seek_prefix == null) | ||||||
{ | ||||||
throw new ArgumentException($"{nameof(key_prefix)} with all bytes being 0xff is not supported now"); | ||||||
} | ||||||
} | ||||||
return FindInternal(key_prefix, seek_prefix, direction); | ||||||
} | ||||||
|
||||||
private IEnumerable<(StorageKey Key, StorageItem Value)> FindInternal(byte[]? key_prefix, byte[]? seek_prefix, SeekDirection direction) | ||||||
{ | ||||||
foreach (var (key, value) in Seek(seek_prefix, direction)) | ||||||
if (key_prefix == null || key.ToArray().AsSpan().StartsWith(key_prefix)) | ||||||
yield return (key, value); | ||||||
else if (direction == SeekDirection.Forward || (seek_prefix == null || !key.ToArray().SequenceEqual(seek_prefix))) | ||||||
yield break; | ||||||
// GetSeekPrefix with 0 for compatibility with old code | ||||||
var seekPrefix = direction == SeekDirection.Forward ? keyPrefix : keyPrefix.GetSeekPrefix(0); | ||||||
return this.ScanPrefix(keyPrefix, seekPrefix, direction); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We dont use |
||||||
} | ||||||
|
||||||
/// <summary> | ||||||
|
@@ -257,14 +221,7 @@ public void Delete(StorageKey key) | |||||
/// <returns>The entries found with the desired range.</returns> | ||||||
public IEnumerable<(StorageKey Key, StorageItem Value)> FindRange(byte[] start, byte[] end, SeekDirection direction = SeekDirection.Forward) | ||||||
{ | ||||||
ByteArrayComparer comparer = direction == SeekDirection.Forward | ||||||
? ByteArrayComparer.Default | ||||||
: ByteArrayComparer.Reverse; | ||||||
foreach (var (key, value) in Seek(start, direction)) | ||||||
if (comparer.Compare(key.ToArray(), end) < 0) | ||||||
yield return (key, value); | ||||||
else | ||||||
yield break; | ||||||
return this.ScanRange(start, end, direction); | ||||||
} | ||||||
|
||||||
/// <summary> | ||||||
|
@@ -420,24 +377,16 @@ public StorageItem GetOrAdd(StorageKey key, Func<StorageItem> factory) | |||||
{ | ||||||
cached = _dictionary | ||||||
.Where(p => p.Value.State != TrackState.Deleted && p.Value.State != TrackState.NotFound && (keyOrPrefix == null || comparer.Compare(p.Key.ToArray(), keyOrPrefix) >= 0)) | ||||||
.Select(p => | ||||||
( | ||||||
KeyBytes: p.Key.ToArray(), | ||||||
p.Key, | ||||||
p.Value.Item | ||||||
)) | ||||||
.Select(p => (KeyBytes: p.Key.ToArray(), p.Key, p.Value.Item)) | ||||||
.OrderBy(p => p.KeyBytes, comparer) | ||||||
.ToArray(); | ||||||
cachedKeySet = new HashSet<StorageKey>(_dictionary.Keys); | ||||||
} | ||||||
|
||||||
var uncached = SeekInternal(keyOrPrefix ?? Array.Empty<byte>(), direction) | ||||||
.Where(p => !cachedKeySet.Contains(p.Key)) | ||||||
.Select(p => | ||||||
( | ||||||
KeyBytes: p.Key.ToArray(), | ||||||
p.Key, | ||||||
p.Value | ||||||
)); | ||||||
.Select(p => (KeyBytes: p.Key.ToArray(), p.Key, p.Value)); | ||||||
|
||||||
using var e1 = cached.GetEnumerator(); | ||||||
using var e2 = uncached.GetEnumerator(); | ||||||
(byte[] KeyBytes, StorageKey Key, StorageItem Item) i1, i2; | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -9,22 +9,32 @@ | |||||
// Redistribution and use in source and binary forms with or without | ||||||
// modifications are permitted. | ||||||
|
||||||
#nullable enable | ||||||
|
||||||
using Neo.SmartContract; | ||||||
using System.Collections.Generic; | ||||||
using System.Diagnostics.CodeAnalysis; | ||||||
using System.Runtime.CompilerServices; | ||||||
|
||||||
namespace Neo.Persistence | ||||||
{ | ||||||
/// <summary> | ||||||
/// A read-only view of a store. | ||||||
/// No cache and lock in this implementation, | ||||||
/// so it is faster in some cases(For example, no repeated reads of the same key). | ||||||
/// </summary> | ||||||
public class ReadOnlyStoreView : IReadOnlyStoreView | ||||||
{ | ||||||
private readonly IReadOnlyStore store; | ||||||
private readonly IReadOnlyStore _store; | ||||||
|
||||||
public ReadOnlyStoreView(IReadOnlyStore store) | ||||||
{ | ||||||
this.store = store; | ||||||
_store = store; | ||||||
} | ||||||
|
||||||
/// <inheritdoc/> | ||||||
public bool Contains(StorageKey key) => store.Contains(key.ToArray()); | ||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||||||
public bool Contains(StorageKey key) => _store.Contains(key.ToArray()); | ||||||
|
||||||
/// <inheritdoc/> | ||||||
public StorageItem this[StorageKey key] | ||||||
|
@@ -38,11 +48,19 @@ public StorageItem this[StorageKey key] | |||||
} | ||||||
|
||||||
/// <inheritdoc/> | ||||||
public bool TryGet(StorageKey key, out StorageItem item) | ||||||
public bool TryGet(StorageKey key, [NotNullWhen(true)] out StorageItem? item) | ||||||
{ | ||||||
var ok = store.TryGet(key.ToArray(), out byte[] value); | ||||||
var ok = _store.TryGet(key.ToArray(), out var value); | ||||||
item = ok ? new StorageItem(value) : null; | ||||||
return ok; | ||||||
} | ||||||
|
||||||
/// <inheritdoc/> | ||||||
public IEnumerable<(StorageKey Key, StorageItem Value)> Seek( | ||||||
byte[]? keyOrPrefix = null, SeekDirection direction = SeekDirection.Forward) | ||||||
{ | ||||||
foreach (var (key, value) in _store.Seek(keyOrPrefix, direction)) | ||||||
yield return (new(key), new(value)); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.