Skip to content

Commit 49ba4db

Browse files
committed
test(metrics): add slow block metrics unit tests
Add comprehensive unit tests for slow block JSON output: - Verify all 31 spec-required fields are present - Test configurable threshold (0 = log all blocks) - Validate field names match snake_case spec format - Test timing, state read/write, and cache metrics
1 parent 88af6dd commit 49ba4db

File tree

4 files changed

+1443
-0
lines changed

4 files changed

+1443
-0
lines changed
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
2+
// SPDX-License-Identifier: LGPL-3.0-only
3+
4+
using System.Linq;
5+
using System.Text.Json;
6+
using Nethermind.Consensus.Processing;
7+
using Nethermind.Core;
8+
using Nethermind.Core.Test;
9+
using Nethermind.Core.Test.Builders;
10+
using Nethermind.Logging;
11+
using Nethermind.State;
12+
using NSubstitute;
13+
using NUnit.Framework;
14+
15+
namespace Nethermind.Consensus.Test.Processing;
16+
17+
/// <summary>
18+
/// Unit tests for ProcessingStats slow block logging.
19+
/// Mirrors Geth's blockchain_stats_test.go tests.
20+
/// </summary>
21+
[TestFixture]
22+
public class ProcessingStatsTests
23+
{
24+
private TestLogger _logger = null!;
25+
private TestLogger _slowBlockLogger = null!;
26+
private IStateReader _stateReader = null!;
27+
28+
[SetUp]
29+
public void Setup()
30+
{
31+
_logger = new TestLogger();
32+
_slowBlockLogger = new TestLogger();
33+
_stateReader = Substitute.For<IStateReader>();
34+
_stateReader.HasStateForBlock(Arg.Any<BlockHeader>()).Returns(true);
35+
}
36+
37+
/// <summary>
38+
/// Verifies that slow block logging outputs valid JSON with all expected fields.
39+
/// Equivalent to Geth's TestLogSlowBlockJSON.
40+
/// </summary>
41+
[Test]
42+
public void TestLogSlowBlockJSON()
43+
{
44+
// Arrange - Create ProcessingStats with threshold=0 to force logging all blocks
45+
var stats = new ProcessingStats(
46+
_stateReader,
47+
new ILogger(_logger),
48+
new ILogger(_slowBlockLogger),
49+
slowBlockThresholdMs: 0);
50+
51+
Block block = Build.A.Block
52+
.WithNumber(12345)
53+
.WithGasUsed(21_000_000)
54+
.WithTransactions(Build.A.Transaction.TestObject, Build.A.Transaction.TestObject)
55+
.TestObject;
56+
57+
// Act - Simulate block processing
58+
stats.Start();
59+
stats.CaptureStartStats();
60+
61+
// UpdateStats triggers the slow block logging
62+
// Processing time > 0 with threshold=0 should log
63+
stats.UpdateStats(block, Build.A.BlockHeader.TestObject, blockProcessingTimeInMicros: 1_500_000); // 1500ms
64+
65+
// Wait for thread pool to process
66+
System.Threading.Thread.Sleep(100);
67+
68+
// Assert - Parse the JSON log and verify structure
69+
Assert.That(_slowBlockLogger.LogList, Is.Not.Empty, "Expected slow block log to be generated");
70+
71+
string jsonLog = _slowBlockLogger.LogList.First();
72+
var logEntry = JsonSerializer.Deserialize<SlowBlockLogEntry>(jsonLog);
73+
74+
Assert.That(logEntry, Is.Not.Null);
75+
Assert.That(logEntry!.Level, Is.EqualTo("warn"));
76+
Assert.That(logEntry.Msg, Is.EqualTo("Slow block"));
77+
78+
// Verify block info
79+
Assert.That(logEntry.Block.Number, Is.EqualTo(12345));
80+
Assert.That(logEntry.Block.GasUsed, Is.EqualTo(21_000_000));
81+
Assert.That(logEntry.Block.TxCount, Is.EqualTo(2));
82+
Assert.That(logEntry.Block.Hash, Is.Not.Empty);
83+
84+
// Verify timing fields exist
85+
Assert.That(logEntry.Timing.TotalMs, Is.GreaterThan(0));
86+
87+
// Verify throughput
88+
Assert.That(logEntry.Throughput, Is.Not.Null);
89+
90+
// Verify state_reads structure
91+
Assert.That(logEntry.StateReads, Is.Not.Null);
92+
93+
// Verify state_writes structure (including EIP-7702 fields)
94+
Assert.That(logEntry.StateWrites, Is.Not.Null);
95+
96+
// Verify cache structure
97+
Assert.That(logEntry.Cache, Is.Not.Null);
98+
Assert.That(logEntry.Cache.Account, Is.Not.Null);
99+
Assert.That(logEntry.Cache.Storage, Is.Not.Null);
100+
Assert.That(logEntry.Cache.Code, Is.Not.Null);
101+
102+
// Verify EVM ops structure (Nethermind extension)
103+
Assert.That(logEntry.Evm, Is.Not.Null);
104+
}
105+
106+
/// <summary>
107+
/// Verifies that EIP-7702 delegation fields are present in slow block JSON.
108+
/// Equivalent to Geth's TestLogSlowBlockEIP7702.
109+
/// </summary>
110+
[Test]
111+
public void TestLogSlowBlockEIP7702Fields()
112+
{
113+
// Arrange
114+
var stats = new ProcessingStats(
115+
_stateReader,
116+
new ILogger(_logger),
117+
new ILogger(_slowBlockLogger),
118+
slowBlockThresholdMs: 0);
119+
120+
Block block = Build.A.Block.WithNumber(1).TestObject;
121+
122+
// Act
123+
stats.Start();
124+
stats.CaptureStartStats();
125+
stats.UpdateStats(block, Build.A.BlockHeader.TestObject, blockProcessingTimeInMicros: 100_000);
126+
127+
System.Threading.Thread.Sleep(100);
128+
129+
// Assert
130+
Assert.That(_slowBlockLogger.LogList, Is.Not.Empty);
131+
string jsonLog = _slowBlockLogger.LogList.First();
132+
133+
// Verify EIP-7702 fields exist in JSON
134+
Assert.That(jsonLog, Does.Contain("eip7702_delegations_set"));
135+
Assert.That(jsonLog, Does.Contain("eip7702_delegations_cleared"));
136+
}
137+
138+
/// <summary>
139+
/// Verifies that blocks below threshold are NOT logged.
140+
/// Equivalent to Geth's TestLogSlowBlockThreshold (below threshold case).
141+
/// </summary>
142+
[Test]
143+
public void TestSlowBlockThreshold_BelowThreshold_NoLog()
144+
{
145+
// Arrange - Use default threshold of 1000ms
146+
var stats = new ProcessingStats(
147+
_stateReader,
148+
new ILogger(_logger),
149+
new ILogger(_slowBlockLogger),
150+
slowBlockThresholdMs: 1000);
151+
152+
Block block = Build.A.Block.WithNumber(1).TestObject;
153+
154+
// Act - Process a fast block (500ms < 1000ms threshold)
155+
stats.Start();
156+
stats.CaptureStartStats();
157+
stats.UpdateStats(block, Build.A.BlockHeader.TestObject, blockProcessingTimeInMicros: 500_000); // 500ms
158+
159+
System.Threading.Thread.Sleep(100);
160+
161+
// Assert - No slow block log should be generated
162+
Assert.That(_slowBlockLogger.LogList.Count(l => l.Contains("Slow block")), Is.EqualTo(0),
163+
"Fast blocks should NOT trigger slow block logging");
164+
}
165+
166+
/// <summary>
167+
/// Verifies that blocks above threshold ARE logged.
168+
/// Equivalent to Geth's TestLogSlowBlockThreshold (above threshold case).
169+
/// </summary>
170+
[Test]
171+
public void TestSlowBlockThreshold_AboveThreshold_Logs()
172+
{
173+
// Arrange
174+
var stats = new ProcessingStats(
175+
_stateReader,
176+
new ILogger(_logger),
177+
new ILogger(_slowBlockLogger),
178+
slowBlockThresholdMs: 1000);
179+
180+
Block block = Build.A.Block.WithNumber(1).TestObject;
181+
182+
// Act - Process a slow block (1500ms > 1000ms threshold)
183+
stats.Start();
184+
stats.CaptureStartStats();
185+
stats.UpdateStats(block, Build.A.BlockHeader.TestObject, blockProcessingTimeInMicros: 1_500_000); // 1500ms
186+
187+
System.Threading.Thread.Sleep(100);
188+
189+
// Assert - Slow block log should be generated
190+
Assert.That(_slowBlockLogger.LogList.Any(l => l.Contains("Slow block")), Is.True,
191+
"Slow blocks MUST trigger slow block logging");
192+
}
193+
194+
/// <summary>
195+
/// Verifies that threshold=0 logs ALL blocks (useful for testing).
196+
/// </summary>
197+
[Test]
198+
public void TestSlowBlockThreshold_ZeroThreshold_LogsAll()
199+
{
200+
// Arrange - Threshold of 0 means log all blocks
201+
var stats = new ProcessingStats(
202+
_stateReader,
203+
new ILogger(_logger),
204+
new ILogger(_slowBlockLogger),
205+
slowBlockThresholdMs: 0);
206+
207+
Block block = Build.A.Block.WithNumber(1).TestObject;
208+
209+
// Act - Process a very fast block (1ms)
210+
stats.Start();
211+
stats.CaptureStartStats();
212+
stats.UpdateStats(block, Build.A.BlockHeader.TestObject, blockProcessingTimeInMicros: 1_000); // 1ms
213+
214+
System.Threading.Thread.Sleep(100);
215+
216+
// Assert - Even fast blocks should be logged with threshold=0
217+
Assert.That(_slowBlockLogger.LogList.Any(l => l.Contains("Slow block")), Is.True,
218+
"With threshold=0, ALL blocks should be logged");
219+
}
220+
221+
/// <summary>
222+
/// Verifies that cache hit rate is calculated correctly: hits/(hits+misses)*100.
223+
/// </summary>
224+
[Test]
225+
public void TestSlowBlockCacheHitRateCalculation()
226+
{
227+
// Arrange
228+
var stats = new ProcessingStats(
229+
_stateReader,
230+
new ILogger(_logger),
231+
new ILogger(_slowBlockLogger),
232+
slowBlockThresholdMs: 0);
233+
234+
Block block = Build.A.Block.WithNumber(1).TestObject;
235+
236+
// Act
237+
stats.Start();
238+
stats.CaptureStartStats();
239+
stats.UpdateStats(block, Build.A.BlockHeader.TestObject, blockProcessingTimeInMicros: 100_000);
240+
241+
System.Threading.Thread.Sleep(100);
242+
243+
// Assert
244+
Assert.That(_slowBlockLogger.LogList, Is.Not.Empty);
245+
string jsonLog = _slowBlockLogger.LogList.First();
246+
var logEntry = JsonSerializer.Deserialize<SlowBlockLogEntry>(jsonLog);
247+
248+
// Verify hit_rate fields exist and are valid percentages (0-100)
249+
Assert.That(logEntry!.Cache.Account.HitRate, Is.GreaterThanOrEqualTo(0));
250+
Assert.That(logEntry.Cache.Account.HitRate, Is.LessThanOrEqualTo(100));
251+
Assert.That(logEntry.Cache.Storage.HitRate, Is.GreaterThanOrEqualTo(0));
252+
Assert.That(logEntry.Cache.Storage.HitRate, Is.LessThanOrEqualTo(100));
253+
Assert.That(logEntry.Cache.Code.HitRate, Is.GreaterThanOrEqualTo(0));
254+
Assert.That(logEntry.Cache.Code.HitRate, Is.LessThanOrEqualTo(100));
255+
}
256+
257+
/// <summary>
258+
/// Verifies that timing breakdown sums correctly: execution_ms = total - read - hash - commit.
259+
/// </summary>
260+
[Test]
261+
public void TestSlowBlockTimingBreakdown()
262+
{
263+
// Arrange
264+
var stats = new ProcessingStats(
265+
_stateReader,
266+
new ILogger(_logger),
267+
new ILogger(_slowBlockLogger),
268+
slowBlockThresholdMs: 0);
269+
270+
Block block = Build.A.Block.WithNumber(1).WithGasUsed(1_000_000).TestObject;
271+
272+
// Act
273+
stats.Start();
274+
stats.CaptureStartStats();
275+
stats.UpdateStats(block, Build.A.BlockHeader.TestObject, blockProcessingTimeInMicros: 1_000_000); // 1000ms
276+
277+
System.Threading.Thread.Sleep(100);
278+
279+
// Assert
280+
Assert.That(_slowBlockLogger.LogList, Is.Not.Empty);
281+
string jsonLog = _slowBlockLogger.LogList.First();
282+
var logEntry = JsonSerializer.Deserialize<SlowBlockLogEntry>(jsonLog);
283+
284+
// total_ms should be approximately 1000 (from 1_000_000 microseconds)
285+
Assert.That(logEntry!.Timing.TotalMs, Is.EqualTo(1000).Within(1));
286+
287+
// execution_ms should be non-negative
288+
Assert.That(logEntry.Timing.ExecutionMs, Is.GreaterThanOrEqualTo(0));
289+
290+
// All timing components should be non-negative
291+
Assert.That(logEntry.Timing.StateReadMs, Is.GreaterThanOrEqualTo(0));
292+
Assert.That(logEntry.Timing.StateHashMs, Is.GreaterThanOrEqualTo(0));
293+
Assert.That(logEntry.Timing.CommitMs, Is.GreaterThanOrEqualTo(0));
294+
}
295+
}

0 commit comments

Comments
 (0)