Skip to content
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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
34 changes: 34 additions & 0 deletions src/Neo.Extensions/ArrayExtensions.cs
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;
}
}
}
157 changes: 157 additions & 0 deletions src/Neo/Extensions/Persistence/ReadOnlyViewExtensions.cs
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
yield return (key, value);
yield return new(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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
yield return (key, value);
yield return new(key, value);

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");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new ArgumentException($"{nameof(keyPrefix)} with all bytes being 0xff is not supported now");
throw new NotSupportedException("Array filled with max value (0xFF)", new ArgumentException(nameof(keyPrefix)));

}
return seekPrefix;
}
}
}
71 changes: 10 additions & 61 deletions src/Neo/Persistence/DataCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return this.ScanPrefix(keyPrefix, seekPrefix, direction);
return ScanPrefix(keyPrefix, seekPrefix, direction);

We dont use this

}

/// <summary>
Expand All @@ -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>
Expand Down Expand Up @@ -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;
Expand Down
20 changes: 19 additions & 1 deletion src/Neo/Persistence/IReadOnlyStoreView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
// 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;

namespace Neo.Persistence
{
Expand Down Expand Up @@ -41,6 +44,21 @@ public interface IReadOnlyStoreView
/// <param name="key">The key to get.</param>
/// <param name="item">The entry if found, null otherwise.</param>
/// <returns>True if the entry exists, false otherwise.</returns>
bool TryGet(StorageKey key, out StorageItem item);
bool TryGet(StorageKey key, [NotNullWhen(true)] out StorageItem? item);

/// <summary>
/// Seeks to the entry with the specified key.
/// <para>
/// If keyPrefix is null or empty, it will seek to the first key(even if the direction is backward).
/// So if seek to the last, keyPrefix should be N * 0xff, and N is the max length of the key.
/// </para>
/// </summary>
/// <param name="keyPrefix">The keyPrefix to be sought.</param>
/// <param name="direction">The direction of seek.</param>
/// <returns>
/// An enumerator containing all the entries after keyPrefix(Forward) or before keyPrefix(Backward).
/// </returns>
IEnumerable<(StorageKey Key, StorageItem Value)> Seek(
byte[]? keyPrefix = null, SeekDirection direction = SeekDirection.Forward);
}
}
28 changes: 23 additions & 5 deletions src/Neo/Persistence/ReadOnlyStoreView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
yield return (new(key), new(value));
yield return new(new(key), new(value));

}
}
}
Loading
Loading