Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Nethermind/Nethermind.Api/IInitConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ public interface IInitConfig : IConfig
DefaultValue = "8192",
HiddenFromDocs = true)]
long HealCanonicalChainDepth { get; set; }

[ConfigItem(
Description = "The EVM instruction-stream cache size, in code entries, accepted range 1 to 1048576. Each entry retains up to 512 KiB (typical entries are far smaller), bounding worst-case retention at size x 512 KiB. The simultaneously-hot code set grows with concurrent eth_call-style request parallelism, and once the cache conflict-evicts, execution falls back to a slower interpreter path — lower this only on memory-constrained setups.",
DefaultValue = "4096")]
int InstructionStreamCacheSize { get; set; }
Comment on lines +113 to +114

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low — the accepted range is now enforced but not documented.

ApplyMemoryHint.cs:34-38 rejects anything outside [1, 1 << 20] at startup, and this [ConfigItem] description is the only operator-facing text for the key. Units and default are documented (per coding-style.md), but the bound is discoverable only by tripping it — a node that fails to start on a config value is a worse discovery mechanism than one sentence here. Suggest appending "Valid range: 1 to 1048576." to the description.

}

public enum DiagnosticMode
Expand Down
1 change: 1 addition & 0 deletions src/Nethermind/Nethermind.Api/InitConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class InitConfig : IInitConfig
public string? DataDir { get; set; }
public bool HealCanonicalChain { get; set; } = false;
public long HealCanonicalChainDepth { get; set; } = 8192;
public int InstructionStreamCacheSize { get; set; } = 4096;

[Obsolete("Use DiagnosticMode with MemDb instead")]
public bool UseMemDb
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,45 @@ public void All_inserted_keys_retrievable_at_various_capacities(int capacity)
}
}

[Test]
public void Refreshed_entry_survives_sustained_churn_in_its_set()
{
const int capacity = 64;
int setCount = (int)BitOperations.RoundUpToPowerOf2((uint)((capacity + Ways - 1) / Ways));
int hashShift = BitOperations.Log2((uint)setCount);
// Every key collides into set 0, so each insert past the way count must evict from it.
static DeterministicHashKey MakeKey(int i, int shift) => new(i, (long)(i + 1) << shift);

AssociativeCache<DeterministicHashKey, TestValue> cache = new(capacity);
AssociativeKeyCache<DeterministicHashKey> keyCache = new(capacity);
DeterministicHashKey hot = MakeKey(0, hashShift);
TestValue hotValue = new();
cache.Set(in hot, hotValue);
keyCache.Set(in hot);

for (int i = 1; i <= 200; i++)
{
// The refreshing lookup gives the hot entry the newest eviction age, so 3-random
// eviction (which removes the oldest of its sample) must never select it. This pins
// the ticker semantics: a recency clock coarse enough to tie a refresh with the
// surrounding churn would make this probabilistic.
using (Assert.EnterMultipleScope())
{
Assert.That(cache.Get(in hot), Is.SameAs(hotValue), $"hot entry evicted after {i - 1} churn inserts");
Assert.That(keyCache.Get(in hot), Is.True, $"hot key evicted after {i - 1} churn inserts");
}
DeterministicHashKey filler = MakeKey(i, hashShift);
cache.Set(in filler, new TestValue());
keyCache.Set(in filler);
}

using (Assert.EnterMultipleScope())
{
Assert.That(cache.Get(in hot), Is.SameAs(hotValue));
Assert.That(keyCache.Get(in hot), Is.True);
}
}

private static DeterministicHashKey[] BuildKeys(int capacity, int count)
{
int setCount = (int)BitOperations.RoundUpToPowerOf2((uint)((capacity + Ways - 1) / Ways));
Expand Down
44 changes: 44 additions & 0 deletions src/Nethermind/Nethermind.Core.Test/Threading/StripedLongTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System.Threading.Tasks;
using Nethermind.Core.Threading;
using NUnit.Framework;

namespace Nethermind.Core.Test.Threading;

[Parallelizable(ParallelScope.All)]
public class StripedLongTests
{
[Test]
public void Sum_is_exact_under_concurrent_mixed_adds()
{
StripedLong counter = new();
const int threads = 8;
const int iterations = 100_000;

Parallel.For(0, threads, _ =>
{
for (int i = 0; i < iterations; i++)
{
counter.Increment();
counter.Add(3);
counter.Add(-2);
}
});

Assert.That(counter.Sum, Is.EqualTo((long)threads * iterations * 2));
}

[Test]
public void Negative_totals_are_representable()
{
StripedLong counter = new();
counter.Add(-5);
counter.Increment();
Assert.That(counter.Sum, Is.EqualTo(-4));
}

[Test]
public void New_counter_sums_to_zero() => Assert.That(new StripedLong().Sum, Is.Zero);
}
22 changes: 11 additions & 11 deletions src/Nethermind/Nethermind.Core/Caching/AssociativeCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -79,13 +80,6 @@ public sealed class AssociativeCache<TKey, TValue>
/// </summary>
private long _epochAndCount;

/// <summary>
/// Monotonic counter for eviction-age tracking. Interlocked.Increment is faster
/// than Stopwatch.GetTimestamp() (RDTSC) — ~7.6ns vs ~19ns single-threaded,
/// and scales better under contention (20% faster at 8 threads).
/// </summary>
private long _ticker;

public int Count => ReadCount(ref _epochAndCount);

public AssociativeCache(int maxCapacity)
Expand Down Expand Up @@ -166,11 +160,18 @@ private bool TryGetCore<TRefreshTicker>(in TKey key, out TValue? value)
if (h1 == h2 && storedKey.Equals(in key))
{
// JIT eliminates this branch entirely per TRefreshTicker instantiation.
// Eviction age uses the high-resolution clock rather than a shared counter: a
// per-hit Interlocked on a cache-wide field is a serialized cross-core RMW under
// concurrent readers (and it dirtied the line _epochAndCount lives on, which
// every TryGet reads first). Single-threaded the clock read loses a few ns to the
// old Interlocked (and more on hosts whose clocksource is not TSC), but it writes
// only this entry's own line, so hits scale with reader count — the regime that
// motivated the change. Do not flip back to a shared counter for the ns.
// Ticker store without the set gate is safe: 8-byte aligned long is atomic on
// x64/ARM64 hardware. A race with a concurrent Set only affects eviction ranking,
// not key/value correctness — the "losing" ticker value is simply slightly stale.
if (TRefreshTicker.IsActive)
e.Ticker = Interlocked.Increment(ref _ticker);
e.Ticker = Stopwatch.GetTimestamp();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low — one residual risk I should have named when I suggested this source.

The change is the one I asked for and the reasoning in the new comment is right. Two things worth recording, since the comment this replaced documented the opposite conclusion ("Interlocked.Increment is faster than Stopwatch.GetTimestamp() (RDTSC) — ~7.6 ns vs ~19 ns single-threaded, and scales better under contention (20% faster at 8 threads)"):

  1. Stopwatch.GetTimestamp() is only cheap when the host's clocksource is TSC. On Linux it is clock_gettime(CLOCK_MONOTONIC); that resolves in the vDSO without a syscall only for tsc-family clocksources. On a VM that has fallen back to xen / hpet / acpi_pm (older Xen-based instances, some nested-virt setups), the same call becomes an MMIO read or a real syscall in the hundreds-of-ns-to-µs range. That would now be paid on every hit of StaticCodeCache / InstructionStreamCache / TxPool.HashCache — per call frame and per tx. Your benchmark box is presumably TSC, so the arms cannot see this.
  2. Because the removed comment recorded a measured decision in the other direction, a future reader has two contradictory claims to reconcile. One clause noting that the earlier number compared the two clocks in isolation, while the cost being removed here is the shared-line RMW next to _epochAndCount, would stop this flipping back.

If (1) is a concern for the node's supported hosts, the cheapest alternative that keeps the old counter's measured single-threaded cost is to keep Interlocked.Increment but move _ticker onto its own cache line (it was adjacent to _epochAndCount, which is the false-sharing cause the comment names) — that addresses the stated mechanism without introducing a clock read on the hit path. Not asking for a change, just recording the tradeoff.

value = storedValue;
return true;
Comment on lines 171 to 176

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Medium — the replacement clock is coarser than the comment implies, and this degrades eviction in the very cache the PR is trying to fix.

Removing the per-hit Interlocked is clearly right. The concern is the choice of replacement source, not the removal.

Environment.TickCount64 is not "millisecond granularity" in practice:

  • Windows: GetTickCount64, which advances at the system timer tick — typically ~15.6 ms.
  • Linux: clock_gettime(CLOCK_MONOTONIC_COARSE), i.e. one jiffy — 1–4 ms depending on CONFIG_HZ.

Ticker is only ever consumed by Pick3RandomEvictEntryPick3RandomEvict(ta, tb, tc, …), which picks the minimum. Under the load this PR targets (300 rps, hot code set), all 8 ways of a set are touched many times inside a single tick, so all three sampled tickers are equal and 3-random-min degenerates to plain random eviction within the set. Previously Interlocked.Increment gave a strictly increasing, unique value per hit, i.e. a genuine approximate-LRU ordering.

That interacts directly with the other half of this PR: Init.InstructionStreamCacheSize exists because the instruction-stream cache conflict-evicts under concurrency — and this change makes the eviction victim choice within a conflicted set worse at the same time. The benchmark arms can't separate the two, since both landed together.

Suggestions, in rough order of preference:

  1. Keep a monotonic-but-cheap source with sub-tick resolution, e.g. Stopwatch.GetTimestamp() >> 16 (~µs–tens-of-µs buckets on x64, no shared line, no RDTSC serialization concerns beyond the read itself) — you already pay Stopwatch.GetTimestamp() on the eviction path below.
  2. Or keep TickCount64 but add a hit-rate measurement for InstructionStreamCache / the state caches before and after, so the eviction-quality cost is quantified rather than assumed.

Either way, a unit test asserting "under a set-conflicting key stream, the least-recently-read entry is evicted more often than chance" would pin the intended behaviour down. Same comment applies to AssociativeKeyCache.cs:100-112.

}
Expand Down Expand Up @@ -232,8 +233,7 @@ private bool SetCore(in TKey key, TValue val, int baseIdx, long hashPart)
{
if ((h & HashMask) == hashPart && e.Key.Equals(in key))
{
long now = Interlocked.Increment(ref _ticker);
WriteEntry(ref e, h, in key, val, tagToStore, now);
WriteEntry(ref e, h, in key, val, tagToStore, Stopwatch.GetTimestamp());
return false;
}
}
Expand All @@ -249,7 +249,7 @@ private bool SetCore(in TKey key, TValue val, int baseIdx, long hashPart)

if (ReadEpoch(ref _epochAndCount) != epochTag) continue;

long timestamp = Interlocked.Increment(ref _ticker);
long timestamp = Stopwatch.GetTimestamp();
int target = bestEmpty >= 0
? bestEmpty
: bestStale >= 0
Expand Down
15 changes: 11 additions & 4 deletions src/Nethermind/Nethermind.Core/Caching/AssociativeKeyCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -29,7 +30,6 @@ public sealed class AssociativeKeyCache<TKey>
private readonly int _hashShift;
private readonly int[] _setGates;
private long _epochAndCount;
private long _ticker;

public int Count => ReadCount(ref _epochAndCount);

Expand Down Expand Up @@ -95,11 +95,18 @@ private bool GetCore<TRefreshTicker>(in TKey key)
if (h1 == h2 && storedKey.Equals(in key))
{
// JIT eliminates this branch entirely per TRefreshTicker instantiation.
// Eviction age uses the high-resolution clock rather than a shared counter: a
// per-hit Interlocked on a cache-wide field is a serialized cross-core RMW under
// concurrent readers (and it dirtied the line _epochAndCount lives on, which
// every Get reads first). Single-threaded the clock read loses a few ns to the
// old Interlocked (and more on hosts whose clocksource is not TSC), but it writes
// only this entry's own line, so hits scale with reader count — the regime that
// motivated the change. Do not flip back to a shared counter for the ns.
// Ticker store without the set gate is safe: 8-byte aligned long is atomic on
// x64/ARM64 hardware. A race with a concurrent Set only affects eviction ranking,
// not key/value correctness — the "losing" ticker value is simply slightly stale.
if (TRefreshTicker.IsActive)
e.Ticker = Interlocked.Increment(ref _ticker);
e.Ticker = Stopwatch.GetTimestamp();
return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved (Medium 2) — with one coverage gap.

Stopwatch.GetTimestamp() is the right source here: I checked that Ticker is consumed only by Pick3RandomEvictEntry's relative min, and that cleared entries reset to 0 (which sorts as oldest), so switching the unit carries no absolute-time semantics. Folding the eviction path's second GetTimestamp() into the single timestamp local is a nice side-effect.

Gap: AssociativeKeyCache got the identical change, but Refreshed_entry_survives_sustained_churn_in_its_set only exercises AssociativeCache. The neighbouring test in the same base class already drives both cache and keyCache side by side, so extending it is a few lines and would cover both implementations of the semantics being pinned.

Two notes on the test itself, neither blocking:

  • It is deterministic by construction rather than by luck — Pick3Indices guarantees three distinct ways, and the hot entry always holds the set's maximum ticker, so the sampled minimum can never be it. Worth knowing the one theoretical hole: Pick3RandomEvict breaks ties toward the earlier index (ta <= tb), so a tie between the hot entry and both other sampled entries could evict it. That needs three iterations inside one clock tick — impossible on Linux (Stopwatch.Frequency == 1e9), and vanishingly unlikely on Windows QPC's 100 ns tick.
  • The per-hit clock read (~15–25 ns via vDSO / QPC) is now on the hit path of the hottest users — StaticCodeCache, InstructionStreamCache, TxPool.HashCache. Those fire per call frame / per tx rather than per opcode, and the expb arms (AVG −1.5% … −3.1%) show no block-processing regression, so this looks paid for; just flagging that it is the new steady-state cost of a hit.

}
}
Expand Down Expand Up @@ -158,7 +165,7 @@ private bool SetCore(in TKey key, int baseIdx, long hashPart)
// Unlike AssociativeCache.SetCore (which calls WriteEntry to update the value),
// the key-only variant has nothing to write, so a bare ticker store suffices.
// The seqlock header is unchanged, which is correct: readers see a stable entry.
e.Ticker = Interlocked.Increment(ref _ticker);
e.Ticker = Stopwatch.GetTimestamp();
return false;
}
}
Expand All @@ -174,7 +181,7 @@ private bool SetCore(in TKey key, int baseIdx, long hashPart)

if (ReadEpoch(ref _epochAndCount) != epochTag) continue;

long timestamp = Interlocked.Increment(ref _ticker);
long timestamp = Stopwatch.GetTimestamp();
int target = bestEmpty >= 0
? bestEmpty
: bestStale >= 0
Expand Down
9 changes: 8 additions & 1 deletion src/Nethermind/Nethermind.Core/Caching/ClockCacheBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@ protected void MarkAccessed(int position)

ref long accessedBitmapWord = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(HasBeenAccessedBitmap), offset);

Interlocked.Or(ref accessedBitmapWord, flags);
// Test first: hot entries are re-marked far more often than the clock hand clears them,
// and an unconditional lock-prefixed Or on a word shared by 64 entries serializes
// concurrent readers. A racing clear between the read and the Or loses at most one
// access mark — the same tolerance the eviction algorithm already has.
if ((Volatile.Read(ref accessedBitmapWord) & flags) != flags)
{
Interlocked.Or(ref accessedBitmapWord, flags);
}
}

protected void MarkAccessedNonConcurrent(int position)
Expand Down
56 changes: 56 additions & 0 deletions src/Nethermind/Nethermind.Core/Threading/StripedLong.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading;

namespace Nethermind.Core.Threading;

/// <summary>
/// Additive counter safe for hot concurrent paths: increments land on a per-core slot, reads sum
/// the slots.
/// </summary>
/// <remarks>
/// A shared counter word turns every increment into a serialized cross-core cache-line transfer
/// once several threads hit it (RPC workers, prewarm workers). Striping by
/// <see cref="Thread.GetCurrentProcessorId"/> keeps the RMW local to the core in the common case;
/// the atomic add only guards against threads that share or migrate between cores. Slots are
/// 128-byte spaced — same isolation as <see cref="CacheLinePaddedLong"/> (adjacent-line prefetch
/// pairs lines) — with a leading pad stride so the first slot is also isolated from whatever
/// precedes the array in memory. Each instance allocates (stripes + 1) * 128 bytes, where stripes
/// is <see cref="Environment.ProcessorCount"/> rounded up to a power of two, captured once at
/// type initialization (later CPU hot-add is not tracked; the mask just folds new ids onto
/// existing slots). Reads are O(stripes) and torn only across slots: fine for metrics, not for
/// invariants.
/// </remarks>
public sealed class StripedLong
{
// 16 longs = 128 bytes between live slots.
private const int SlotStride = 16;
private static readonly int s_stripeMask = (int)BitOperations.RoundUpToPowerOf2((uint)Environment.ProcessorCount) - 1;

private readonly long[] _slots = new long[(s_stripeMask + 2) * SlotStride];

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(long value)
Comment on lines +30 to +37

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low — a few notes on the new primitive; no correctness problems found.

Add/Sum are correct, Add(-1) works (individual slots go negative, Sum stays right), and the 128-byte stride matches CacheLinePaddedLong. Three small things:

  1. Slot 0 is not line-isolated on its low side. _slots[0] sits at offset 16 in the array object (header + length), so the 64-byte line containing it also contains the array header and whatever object precedes it on the heap. In this PR the StripedLong statics are allocated back to back, so the preceding object is the previous instance's trailing 128 bytes of padding — safe today, but incidental. Skipping index 0 ((idx + 1) * SlotStride) or padding the head would make it structural.

  2. Environment.ProcessorCount is captured in a static readonly at type-init. With container CPU-limit changes or DOTNET_PROCESSOR_COUNT, Thread.GetCurrentProcessorId() can exceed the mask, which is harmless (two cores share a slot) — worth a sentence in <remarks> so nobody "fixes" it later.

  3. Memory. RoundUpToPowerOf2(ProcessorCount) * 128 bytes per instance: 16 KiB each on a 96-core box. With ~15 static counters plus 2 per DbOnTheRocks instance that's a few hundred KiB — fine, but the <remarks> currently only mentions read cost, not allocation size. Worth adding, since the type is now easy to reach for.

There's no unit test for this type. A concurrent-Add-from-N-threads-sums-exactly test plus a negative-Add test would be cheap and would lock in the contract that Sum is exact (not approximate) once writers quiesce — which DbOnTheRocksTests now depends on.

=> Interlocked.Add(ref _slots[((Thread.GetCurrentProcessorId() & s_stripeMask) + 1) * SlotStride], value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Increment() => Add(1);

public long Sum
{
get
{
long[] slots = _slots;
long sum = 0;
for (int stripe = 0; stripe <= s_stripeMask; stripe++)
{
sum += Volatile.Read(ref slots[(stripe + 1) * SlotStride]);
}
return sum;
}
}
}
26 changes: 17 additions & 9 deletions src/Nethermind/Nethermind.Db.Rocks/DbOnTheRocks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ public partial class DbOnTheRocks : IDb, ITunableDb, IReadOnlyNativeKeyValueStor

private readonly List<IDisposable> _metricsUpdaters = [];

internal CacheLinePaddedLong _allocatedSpan;
private CacheLinePaddedLong _totalReads;
// Striped: every concurrent reader (RPC workers, prewarm workers) updates these per Get, and
// a single shared word per DB serializes them under load.
internal readonly StripedLong _allocatedSpan = new();
private readonly StripedLong _totalReads = new();
private CacheLinePaddedLong _totalWrites;

private readonly DisposableLazy<IteratorManager>? _iteratorManager;
Expand Down Expand Up @@ -358,7 +360,7 @@ private void RepairIfCorrupted(DbOptions dbOptions)
_fileSystem.File.Delete(corruptMarker);
}

protected internal void UpdateReadMetrics() => Interlocked.Increment(ref _totalReads.Value);
protected internal void UpdateReadMetrics() => _totalReads.Increment();

protected internal void UpdateWriteMetrics() => Interlocked.Increment(ref _totalWrites.Value);

Expand All @@ -377,7 +379,7 @@ public IDbMeta.DbMetric GatherMetric()
CacheSize = 0,
IndexSize = 0,
MemtableSize = 0,
TotalReads = _totalReads.Value,
TotalReads = _totalReads.Sum,
TotalWrites = _totalWrites.Value,
};
}
Expand All @@ -387,7 +389,7 @@ public IDbMeta.DbMetric GatherMetric()
CacheSize = GetCacheSize(),
IndexSize = GetIndexSize(),
MemtableSize = GetMemtableSize(),
TotalReads = _totalReads.Value,
TotalReads = _totalReads.Sum,
TotalWrites = _totalWrites.Value,
};
}
Expand Down Expand Up @@ -951,8 +953,12 @@ internal Span<byte> GetSpanWithColumnFamily(scoped ReadOnlySpan<byte> key, Colum

if (!span.IsNullOrEmpty())
{
Interlocked.Increment(ref _allocatedSpan.Value);
GC.AddMemoryPressure(span.Length);
_allocatedSpan.Increment();
// Pressure hints exist so the GC accounts for sizeable native memory held alive by
// managed wrappers. Sub-threshold spans are transient (released within the request)
// and each Add/Remove pair mutates GC-global accounting — a contended cost per DB
// read under concurrent load. The threshold must match DangerousReleaseMemory.
if (span.Length >= GcPressureSpanThreshold) GC.AddMemoryPressure(span.Length);
}
return span;
}
Expand Down Expand Up @@ -1047,12 +1053,14 @@ internal void MergeWithColumnFamily(ReadOnlySpan<byte> key, ColumnFamilyHandle?
}
}

private const int GcPressureSpanThreshold = 16 * 1024;

public void DangerousReleaseMemory(in ReadOnlySpan<byte> span)
{
if (!span.IsNullOrEmpty())
{
Interlocked.Decrement(ref _allocatedSpan.Value);
GC.RemoveMemoryPressure(span.Length);
_allocatedSpan.Add(-1);
if (span.Length >= GcPressureSpanThreshold) GC.RemoveMemoryPressure(span.Length);
}
_db.DangerousReleaseMemory(span);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Nethermind/Nethermind.Db.Test/DbOnTheRocksTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -429,10 +429,10 @@ private long AllocatedSpan
{
if (_db is ColumnDb columnDb)
{
return columnDb._mainDb._allocatedSpan.Value;
return columnDb._mainDb._allocatedSpan.Sum;
}

return (_db as DbOnTheRocks)._allocatedSpan.Value;
return (_db as DbOnTheRocks)._allocatedSpan.Sum;
}
}

Expand Down
Loading
Loading