-
Notifications
You must be signed in to change notification settings - Fork 720
perf(rpc): cut eth_call overhead in memory, code stream, and tx parse #12716
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
base: master
Are you sure you want to change the base?
Changes from 3 commits
8994d58
fdc7da2
f58ff48
54e9009
a05ef7c
e1c5c1b
67712f4
1f3fc9d
c711e6d
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -102,9 +102,12 @@ public void CalculateMemoryCost_LengthExceedsULong_ShouldReturnOutOfGas() | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.That(result, Is.EqualTo(0UL)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [TestCase(1024)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [TestCase(4096)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [TestCase(32 * 1024)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [TestCase(70 * 1024)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [TestCase(2 * 1024 * 1024)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void Large_pooled_buffer_is_zeroed_on_reuse(int size) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void Pooled_buffer_is_zeroed_on_reuse(int size) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EvmPooledMemory dirty = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UInt256 zero = UInt256.Zero; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -121,6 +124,63 @@ public void Large_pooled_buffer_is_zeroed_on_reuse(int size) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clean.Dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [Test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void Grow_after_dirty_reuse_preserves_written_prefix_and_zeroes_tail() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const int firstSize = 2048; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const int secondSize = 8192; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EvmPooledMemory dirty = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Span<byte> pattern = new byte[firstSize]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pattern.Fill(0xaa); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.That(dirty.TrySave(UInt256.Zero, pattern), Is.True); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dirty.Dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EvmPooledMemory next = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| byte[] firstWord = TestItem.KeccakA.BytesToArray(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.That(next.TrySaveWord(UInt256.Zero, firstWord), Is.True); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UInt256 growOffset = (UInt256)(secondSize - EvmPooledMemory.WordSize); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.That(next.TryLoadSpan(in growOffset, (UInt256)EvmPooledMemory.WordSize, out Span<byte> tail), Is.True); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.That(tail.ToArray(), Is.EqualTo(new byte[EvmPooledMemory.WordSize])); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.That(next.TryLoadSpan(UInt256.Zero, (UInt256)EvmPooledMemory.WordSize, out Span<byte> head), Is.True); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.That(head.ToArray(), Is.EqualTo(firstWord)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finally | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next.Dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [Test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void GetTrace_slice_past_size_does_not_leak_dirty_bytes() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const int dirtySize = 1024; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EvmPooledMemory dirty = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Span<byte> pattern = new byte[dirtySize]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pattern.Fill(0xff); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.That(dirty.TrySave(UInt256.Zero, pattern), Is.True); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dirty.Dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EvmPooledMemory memory = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| memory.CalculateMemoryCost(0, 32, out bool outOfGas); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.That(outOfGas, Is.False); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TraceMemory trace = memory.GetTrace(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.That(trace.Size, Is.EqualTo(32UL)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert.That(trace.Slice(0, 512).ToArray(), Is.EqualTo(new byte[512])); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
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. Medium — this regression test passes on the unfixed code, so it does not guard the 1. The dirty buffer is never rented. 2. Even with a real rent, 1 KiB can't hold a dirty byte. To make it discriminate, force an actual rent and use a buffer bigger than the 4 KiB zero chunk:
Suggested change
Contributor
Author
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. Fixed: test now dirties 32 KiB (past the 4 KiB zero chunk), forces a real rent via |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finally | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| memory.Dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [Test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void CalculateMemoryCost_LengthExceedsLongMax_ShouldReturnOutOfGas() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,7 +78,13 @@ public CodeInfo(IPrecompile? precompile) | |
| { | ||
| if (Volatile.Read(ref _streamBuildState) == StreamBuildUnavailable) | ||
| return null; | ||
| if (CodeHash != default && InstructionStreamCache.TryGet(CodeHash, out InstructionStream? cached)) | ||
| // No cache key (e.g. eth_call overrides) — building would only be discarded. | ||
| if (CodeHash == default) | ||
| { | ||
| Volatile.Write(ref _streamBuildState, StreamBuildUnavailable); | ||
| return null; | ||
| } | ||
|
Contributor
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. Medium — latching
The // No cache key (e.g. eth_call overrides) — building would only be discarded.
if (CodeHash == default)
return null;If you do want to keep the latch,
Contributor
Author
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. Fixed: skip is now stateless ( |
||
| if (InstructionStreamCache.TryGet(CodeHash, out InstructionStream? cached)) | ||
| return cached; | ||
| if (Interlocked.Increment(ref _streamHits) < StreamInterpreter.BuildThreshold) | ||
| return null; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -397,7 +397,11 @@ public TraceMemory GetTrace() | |
| { | ||
| ulong size = Size; | ||
| ClearForTracing(size); | ||
| return new(size, _memory); | ||
| // Clamp to Size so TraceMemory.Slice past the EVM high-water cannot see dirty tail bytes. | ||
| if (_memory is null || size == 0) | ||
| return new(size, default); | ||
|
LukaszRozmej marked this conversation as resolved.
|
||
| int visible = (int)Math.Min(size, (ulong)_memory.Length); | ||
| return new(size, _memory.AsMemory(0, visible)); | ||
| } | ||
|
|
||
| public void Dispose() | ||
|
|
@@ -407,7 +411,7 @@ public void Dispose() | |
| if (memory is not null) | ||
| { | ||
| _memory = null; | ||
| ReturnClean(memory, (int)Math.Min(Size, (ulong)memory.Length)); | ||
| Return(memory); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -453,50 +457,48 @@ private void EnsureRented() | |
|
|
||
| private const int MinRentSize = 1_024; | ||
| private const int MaxCachedArrayLength = 1 << 16; | ||
| private const int CleanCacheSlots = 16; | ||
| private const int CacheSlots = 16; | ||
|
|
||
| [ThreadStatic] private static byte[]?[]? _cleanArrays; | ||
| [ThreadStatic] private static int _cleanArrayCount; | ||
| [ThreadStatic] private static byte[]?[]? _cachedArrays; | ||
| [ThreadStatic] private static int _cachedArrayCount; | ||
|
|
||
| private static byte[] RentClean(int minLength) | ||
| // Cached dirty; RentSlow zero-extends past Size in chunks on growth. | ||
| private static byte[] Rent(int minLength) | ||
| { | ||
| byte[]?[]? cache = _cleanArrays; | ||
| int cleanArrayCount = _cleanArrayCount - 1; | ||
| for (int i = cleanArrayCount; i >= 0; i--) | ||
| byte[]?[]? cache = _cachedArrays; | ||
| int cachedArrayCount = _cachedArrayCount - 1; | ||
| for (int i = cachedArrayCount; i >= 0; i--) | ||
| { | ||
| byte[] candidate = cache![i]!; | ||
| if (candidate.Length >= minLength) | ||
| { | ||
| _cleanArrayCount = cleanArrayCount; | ||
| cache[i] = cache[cleanArrayCount]; | ||
| cache[cleanArrayCount] = null; | ||
| _cachedArrayCount = cachedArrayCount; | ||
| cache[i] = cache[cachedArrayCount]; | ||
| cache[cachedArrayCount] = null; | ||
| return candidate; | ||
| } | ||
| } | ||
|
|
||
| if (minLength > MaxCachedArrayLength) | ||
| { | ||
| byte[] pooled = RentLarge(minLength); | ||
| Array.Clear(pooled); | ||
| return pooled; | ||
| return RentLarge(minLength); | ||
| } | ||
|
|
||
| return new byte[BitOperations.RoundUpToPowerOf2((uint)minLength)]; | ||
| } | ||
|
|
||
| private static void ReturnClean(byte[] array, int dirtyLength) | ||
| private static void Return(byte[] array) | ||
| { | ||
| if (array.Length > MaxCachedArrayLength) | ||
| { | ||
| ReturnLarge(array); | ||
| return; | ||
| } | ||
|
|
||
| byte[]?[] cache = _cleanArrays ??= new byte[CleanCacheSlots][]; | ||
| if (_cleanArrayCount < CleanCacheSlots) | ||
| byte[]?[] cache = _cachedArrays ??= new byte[CacheSlots][]; | ||
| if (_cachedArrayCount < CacheSlots) | ||
| { | ||
| Array.Clear(array, 0, dirtyLength); | ||
| cache[_cleanArrayCount++] = array; | ||
| cache[_cachedArrayCount++] = array; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -528,19 +530,29 @@ private static void ReturnLarge(byte[] array) | |
| [MethodImpl(MethodImplOptions.NoInlining)] | ||
| private void RentSlow() | ||
| { | ||
| if (_memory is null) | ||
| byte[]? memory = _memory; | ||
| if (memory is null) | ||
| { | ||
| _memory = RentClean((int)Math.Max((uint)Size, MinRentSize)); | ||
| _memory = memory = Rent((int)Math.Max((uint)Size, MinRentSize)); | ||
| _lastZeroedSize = 0; | ||
| } | ||
| else if (Size > (ulong)_memory.LongLength) | ||
| else if (Size > (ulong)memory.LongLength) | ||
| { | ||
| byte[] beforeResize = _memory; | ||
| _memory = RentClean(TruncateToInt32(Size)); | ||
| Array.Copy(beforeResize, 0, _memory, 0, beforeResize.Length); | ||
| ReturnClean(beforeResize, beforeResize.Length); | ||
| byte[] grown = Rent(TruncateToInt32(Size)); | ||
| Array.Copy(memory, 0, grown, 0, (int)_lastZeroedSize); | ||
| Return(memory); | ||
| _memory = memory = grown; | ||
| } | ||
|
|
||
| _lastZeroedSize = (ulong)_memory.Length; | ||
| ulong size = Size; | ||
| if (size > _lastZeroedSize) | ||
| { | ||
| // Over-zero to a chunk boundary so sequential MSTORE growth does not take RentSlow per word. | ||
| const ulong zeroChunk = 4 * 1024; | ||
| ulong target = Math.Min((ulong)memory.Length, (size + (zeroChunk - 1)) & ~(zeroChunk - 1)); | ||
| Array.Clear(memory, (int)_lastZeroedSize, (int)(target - _lastZeroedSize)); | ||
| _lastZeroedSize = target; | ||
| } | ||
|
Comment on lines
+553
to
+561
Contributor
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. Medium — this moves
Failure scenario: a contract that ABI-encodes sequentially ( The PR reports only an eth_call RPC sweep (−2.7% avg). This is the hot path for all block processing, so please run the reproducible payload benchmark ( A cheap way to keep the deferred-clear win while amortising the call overhead is to zero-extend to a chunk boundary rather than exactly to ulong size = Size;
if (size > _lastZeroedSize)
{
// Over-zero to a chunk boundary so sequential growth doesn't take RentSlow on every word.
const ulong ZeroChunk = 4 * 1024;
ulong target = Math.Min((ulong)memory.Length, (size + (ZeroChunk - 1)) & ~(ZeroChunk - 1));
Array.Clear(memory, (int)_lastZeroedSize, (int)(target - _lastZeroedSize));
_lastZeroedSize = target;
}
Contributor
Author
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. Fixed in f58ff48 (amortization part).
On the payload-benchmark ask: agreed this is block-processing hot path; will follow up with expb numbers (or gate on CI) before merge. The chunking change is the cheap insurance in the meantime. |
||
| } | ||
|
|
||
| // (int)(uint)value rather than (int)value: RyuJIT emits noticeably worse codegen for a | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,9 +4,11 @@ | |||||||
| using System; | ||||||||
| using System.Collections.Generic; | ||||||||
| using System.Linq; | ||||||||
| using System.Numerics; | ||||||||
| using System.Reflection; | ||||||||
| using System.Text; | ||||||||
| using System.Text.Json; | ||||||||
| using System.Text.Json.Nodes; | ||||||||
|
Check warning on line 11 in src/Nethermind/Nethermind.Facade/Eth/RpcTransaction/TransactionForRpc.cs
|
||||||||
|
Contributor
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. Low —
Suggested change
|
||||||||
| using System.Text.Json.Serialization; | ||||||||
| using Nethermind.Core; | ||||||||
| using Nethermind.Core.Crypto; | ||||||||
|
|
@@ -158,7 +160,8 @@ | |||||||
| TxType = T.TxType, | ||||||||
| Type = txType, | ||||||||
| FromTransactionFunc = T.FromTransaction, | ||||||||
| DiscriminatorProperties = uniqueProperties | ||||||||
| DiscriminatorProperties = uniqueProperties, | ||||||||
| DiscriminatorPropertiesUtf8 = Array.ConvertAll(uniqueProperties, static p => Encoding.UTF8.GetBytes(p.ToLowerInvariant())) | ||||||||
| }; | ||||||||
|
|
||||||||
| int existingTypeInfo = _txTypes.FindIndex(t => t.TxType == typeInfo.TxType); | ||||||||
|
|
@@ -184,12 +187,10 @@ | |||||||
|
|
||||||||
| public override TransactionForRpc? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||||||||
| { | ||||||||
| // Copy the reader so we can do a double parse: | ||||||||
| // The first parse is used to check for fields, while the second parses the entire Transaction | ||||||||
| // Peek property names for the concrete type, then deserialize (no DOM). | ||||||||
| Utf8JsonReader txTypeReader = reader; | ||||||||
| JsonObject untyped = JsonSerializer.Deserialize<JsonObject>(ref txTypeReader, options); | ||||||||
|
|
||||||||
| Type concreteTxType = DeriveTxType(untyped, options, out bool isDefaulted); | ||||||||
| Type concreteTxType = DeriveTxType(ref txTypeReader, options, out bool isDefaulted); | ||||||||
|
|
||||||||
| TransactionForRpc? result = (TransactionForRpc?)JsonSerializer.Deserialize(ref reader, concreteTxType, options); | ||||||||
| if (result is not null) | ||||||||
|
|
@@ -199,29 +200,76 @@ | |||||||
| return result; | ||||||||
| } | ||||||||
|
|
||||||||
| private Type DeriveTxType(JsonObject untyped, JsonSerializerOptions options, out bool isDefaulted) | ||||||||
| private static ReadOnlySpan<byte> TypeFieldUtf8 => "type"u8; | ||||||||
| private static ReadOnlySpan<byte> GasPriceFieldUtf8 => "gasprice"u8; | ||||||||
|
|
||||||||
| private Type DeriveTxType(ref Utf8JsonReader reader, JsonSerializerOptions options, out bool isDefaulted) | ||||||||
| { | ||||||||
| const string gasPriceFieldKey = nameof(LegacyTransactionForRpc.GasPrice); | ||||||||
| const string typeFieldKey = nameof(TransactionForRpc.Type); | ||||||||
| TxType? setType = null; | ||||||||
| bool hasGasPrice = false; | ||||||||
| // Bit i set ⇒ discriminator for _txTypes[i] seen; lowest bit wins (registration order). | ||||||||
| ulong discriminated = 0; | ||||||||
|
|
||||||||
| if (reader.TokenType == JsonTokenType.StartObject) | ||||||||
| { | ||||||||
| while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName) | ||||||||
| { | ||||||||
| if (setType is null && NameEqualsIgnoreCase(ref reader, TypeFieldUtf8)) | ||||||||
| { | ||||||||
| reader.Read(); | ||||||||
| setType = JsonSerializer.Deserialize<TxType?>(ref reader, options); | ||||||||
| continue; | ||||||||
|
Contributor
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. Low (perf, free win) — once
Suggested change
Contributor
Author
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. Fixed: break the property scan once |
||||||||
| } | ||||||||
|
|
||||||||
| if (!hasGasPrice && NameEqualsIgnoreCase(ref reader, GasPriceFieldUtf8)) | ||||||||
| { | ||||||||
| hasGasPrice = true; | ||||||||
| } | ||||||||
| else | ||||||||
| { | ||||||||
| int count = Math.Min(_txTypes.Count, 64); | ||||||||
| for (int i = 0; i < count; i++) | ||||||||
| { | ||||||||
| foreach (byte[] discriminator in _txTypes[i].DiscriminatorPropertiesUtf8) | ||||||||
| { | ||||||||
| if (NameEqualsIgnoreCase(ref reader, discriminator)) | ||||||||
| { | ||||||||
| discriminated |= 1UL << i; | ||||||||
| break; | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| reader.Read(); | ||||||||
| reader.Skip(); | ||||||||
|
Contributor
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. Low — Per the BCL contract, Today all reachable call sites are safe — the JSON-RPC pipeline builds its reader with
Suggested change
Contributor
Author
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. Fixed: |
||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| if (untyped.TryGetPropertyValue(typeFieldKey, out JsonNode? node)) | ||||||||
| Type? viaDiscriminator = null; | ||||||||
| if (discriminated != 0) | ||||||||
| { | ||||||||
| TxType? setType = node.Deserialize<TxType?>(options); | ||||||||
| if (setType is not null) | ||||||||
| viaDiscriminator = _txTypes[BitOperations.TrailingZeroCount(discriminated)].Type; | ||||||||
| } | ||||||||
|
|
||||||||
| if (setType is not null) | ||||||||
| { | ||||||||
| isDefaulted = false; | ||||||||
| foreach (TxTypeInfo candidate in _txTypes) | ||||||||
| { | ||||||||
| isDefaulted = false; | ||||||||
| return _txTypes.FirstOrDefault(p => p.TxType == setType)?.Type ?? throw new JsonException("Unknown transaction type"); | ||||||||
| if (candidate.TxType == setType) return candidate.Type; | ||||||||
| } | ||||||||
|
|
||||||||
| throw new JsonException("Unknown transaction type"); | ||||||||
| } | ||||||||
|
|
||||||||
| if (untyped.ContainsKey(gasPriceFieldKey)) | ||||||||
| if (hasGasPrice) | ||||||||
| { | ||||||||
| isDefaulted = true; | ||||||||
| return typeof(LegacyTransactionForRpc); | ||||||||
| } | ||||||||
|
|
||||||||
| // Discriminator field is a strong signal — not a default. | ||||||||
| Type? viaDiscriminator = _txTypes.FirstOrDefault(p => p.DiscriminatorProperties.Any(untyped.ContainsKey))?.Type; | ||||||||
| if (viaDiscriminator is not null) | ||||||||
| { | ||||||||
| isDefaulted = false; | ||||||||
|
|
@@ -232,6 +280,23 @@ | |||||||
| return typeof(EIP1559TransactionForRpc); | ||||||||
| } | ||||||||
|
|
||||||||
| private static bool NameEqualsIgnoreCase(ref Utf8JsonReader reader, ReadOnlySpan<byte> lowerCaseName) | ||||||||
| { | ||||||||
| if (reader.HasValueSequence || reader.ValueIsEscaped) | ||||||||
| { | ||||||||
| return string.Equals(reader.GetString(), Encoding.UTF8.GetString(lowerCaseName), StringComparison.OrdinalIgnoreCase); | ||||||||
|
Contributor
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. Low — the escaped-name fallback allocates two strings per comparison, and it is called once per (property × discriminator) pair.
Also worth noting for future maintainers: the
Contributor
Author
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. Fixed: hot path stays allocation-free; escaped path tries |
||||||||
| } | ||||||||
|
|
||||||||
| ReadOnlySpan<byte> name = reader.ValueSpan; | ||||||||
| if (name.Length != lowerCaseName.Length) return false; | ||||||||
| for (int i = 0; i < name.Length; i++) | ||||||||
| { | ||||||||
| if ((name[i] | 0x20) != lowerCaseName[i]) return false; | ||||||||
| } | ||||||||
|
|
||||||||
| return true; | ||||||||
| } | ||||||||
|
|
||||||||
| public override void Write(Utf8JsonWriter writer, TransactionForRpc value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, value.GetType(), options); | ||||||||
|
|
||||||||
| public static TransactionForRpc FromTransaction(Transaction tx, in TransactionForRpcContext extraData) => _txTypes.FirstOrDefault(t => t.TxType == tx.Type)?.FromTransactionFunc(tx, extraData) | ||||||||
|
|
@@ -243,6 +308,7 @@ | |||||||
| public Type Type { get; set; } | ||||||||
| public FromTransactionFunc FromTransactionFunc { get; set; } | ||||||||
| public string[] DiscriminatorProperties { get; set; } = []; | ||||||||
| public byte[][] DiscriminatorPropertiesUtf8 { get; set; } = []; | ||||||||
|
Contributor
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. Low — After the rewrite, Separately:
Contributor
Author
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. Fixed: removed dead |
||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
|
|
||||||||
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.
Low — this test passes unchanged on
master, so it doesn't guard the optimisation it's named after.On
master,GetOrBuildStream()with a defaultCodeHashalso returnsnullon every call: the cache lookup is skipped by theCodeHash != defaultguard, the firstBuildThreshold - 1calls returnnullon the hit counter, and the call that crosses the threshold schedules a background build thatBuildStreamthen refuses to publish (CodeHash != defaultfails) — returningnulltoo. So every assertion here holds both before and after the change.To actually pin the behaviour, assert the observable difference: that no build was scheduled. E.g. expose/observe
_streamBuildStatetransitioning toStreamBuildUnavailableon the very first call, or assertInstructionStreamCachegained no entry and no work item ran (the siblingInstructionStreamTests.cs:490already uses aSpinUntilonGetOrBuildStreamand could be inverted here).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.
Fixed: test now uses
BuildThreshold = 1, asserts no stream appears for defaultCodeHashwithin a short spin, and a hashed control does build.