From 8a81266787dccb94a4f3141197e959458e649359 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Mon, 27 Jan 2025 08:51:30 +0000 Subject: [PATCH 1/5] Nethermind UI --- .gitignore | 2 + .../Nethermind.Blockchain/BlockTree.cs | 6 + .../Nethermind.Blockchain/BlockTreeOverlay.cs | 15 + .../Nethermind.Blockchain/IBlockTree.cs | 8 + .../ReadOnlyBlockTree.cs | 6 + .../Processing/BlockchainProcessor.cs | 6 + .../Processing/IBlockchainProcessor.cs | 1 + .../Processing/OneTimeProcessor.cs | 1 + .../Processing/ProcessingStats.cs | 61 +- .../Nethermind.Runner.Test/StandardTests.cs | 2 +- .../Nethermind.Runner/ConsoleHelpers.cs | 131 ++++ .../Ethereum/JsonRpcRunner.cs | 1 + .../Nethermind.Runner/JsonRpc/Startup.cs | 17 +- .../Nethermind.Runner/Monitoring/DataFeed.cs | 245 ++++++ .../Monitoring/DataFeedExtensions.cs | 26 + .../Monitoring/NethermindNodeData.cs | 19 + .../Monitoring/TxPool/Link.cs | 11 + .../Monitoring/TxPool/Node.cs | 12 + .../Monitoring/TxPool/TxPoolFlow.cs | 140 ++++ .../Monitoring/TxPool/TxPoolStages.cs | 36 + src/Nethermind/Nethermind.Runner/NLog.config | 1 + .../Nethermind.Runner.csproj | 19 + src/Nethermind/Nethermind.Runner/Program.cs | 2 + src/Nethermind/Nethermind.Runner/package.json | 24 + .../Nethermind.Runner/scripts/app.ts | 262 +++++++ .../Nethermind.Runner/scripts/format.ts | 38 + .../Nethermind.Runner/scripts/sankeyTypes.ts | 33 + .../Nethermind.Runner/scripts/sparkline.ts | 132 ++++ .../Nethermind.Runner/scripts/txPoolFlow.ts | 241 ++++++ .../Nethermind.Runner/scripts/types.ts | 57 ++ .../Nethermind.Runner/tsconfig.json | 24 + .../Nethermind.Runner/wwwroot/css/app.css | 236 ++++++ .../Nethermind.Runner/wwwroot/favicon.webp | Bin 0 -> 9980 bytes .../wwwroot/fonts/dm-mono-latin-ext.woff2 | Bin 0 -> 9516 bytes .../wwwroot/fonts/dm-mono-latin.woff2 | Bin 0 -> 14872 bytes .../wwwroot/fonts/dm-sans-latin-ext.woff2 | Bin 0 -> 31020 bytes .../wwwroot/fonts/dm-sans-latin.woff2 | Bin 0 -> 62792 bytes .../wwwroot/fonts/exo-latin-ext.woff2 | Bin 0 -> 11108 bytes .../wwwroot/fonts/exo-latin.woff2 | Bin 0 -> 12248 bytes .../Nethermind.Runner/wwwroot/index.html | 146 ++++ .../Nethermind.Runner/wwwroot/nethermind.svg | 62 ++ src/Nethermind/Nethermind.Runner/yarn.lock | 711 ++++++++++++++++++ .../Filters/NullHashTxFilter.cs | 1 + .../Nethermind.TxPool/Filters/SizeTxFilter.cs | 1 + src/Nethermind/Nethermind.TxPool/Metrics.cs | 6 + src/Nethermind/Nethermind.TxPool/TxPool.cs | 47 +- src/Nethermind/Nethermind.sln | 18 +- 47 files changed, 2765 insertions(+), 42 deletions(-) create mode 100644 src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs create mode 100644 src/Nethermind/Nethermind.Runner/Monitoring/DataFeedExtensions.cs create mode 100644 src/Nethermind/Nethermind.Runner/Monitoring/NethermindNodeData.cs create mode 100644 src/Nethermind/Nethermind.Runner/Monitoring/TxPool/Link.cs create mode 100644 src/Nethermind/Nethermind.Runner/Monitoring/TxPool/Node.cs create mode 100644 src/Nethermind/Nethermind.Runner/Monitoring/TxPool/TxPoolFlow.cs create mode 100644 src/Nethermind/Nethermind.Runner/Monitoring/TxPool/TxPoolStages.cs create mode 100644 src/Nethermind/Nethermind.Runner/package.json create mode 100644 src/Nethermind/Nethermind.Runner/scripts/app.ts create mode 100644 src/Nethermind/Nethermind.Runner/scripts/format.ts create mode 100644 src/Nethermind/Nethermind.Runner/scripts/sankeyTypes.ts create mode 100644 src/Nethermind/Nethermind.Runner/scripts/sparkline.ts create mode 100644 src/Nethermind/Nethermind.Runner/scripts/txPoolFlow.ts create mode 100644 src/Nethermind/Nethermind.Runner/scripts/types.ts create mode 100644 src/Nethermind/Nethermind.Runner/tsconfig.json create mode 100644 src/Nethermind/Nethermind.Runner/wwwroot/css/app.css create mode 100644 src/Nethermind/Nethermind.Runner/wwwroot/favicon.webp create mode 100644 src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-mono-latin-ext.woff2 create mode 100644 src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-mono-latin.woff2 create mode 100644 src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-sans-latin-ext.woff2 create mode 100644 src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-sans-latin.woff2 create mode 100644 src/Nethermind/Nethermind.Runner/wwwroot/fonts/exo-latin-ext.woff2 create mode 100644 src/Nethermind/Nethermind.Runner/wwwroot/fonts/exo-latin.woff2 create mode 100644 src/Nethermind/Nethermind.Runner/wwwroot/index.html create mode 100644 src/Nethermind/Nethermind.Runner/wwwroot/nethermind.svg create mode 100644 src/Nethermind/Nethermind.Runner/yarn.lock diff --git a/.gitignore b/.gitignore index 3e95fc13ce6..cee0fcbe794 100644 --- a/.gitignore +++ b/.gitignore @@ -404,3 +404,5 @@ FodyWeavers.xsd ## Nethermind keystore/ /.githooks +bundle.js +src/Nethermind/Nethermind.Runner/wwwroot/js.map diff --git a/src/Nethermind/Nethermind.Blockchain/BlockTree.cs b/src/Nethermind/Nethermind.Blockchain/BlockTree.cs index acb74922fdb..f279fb994bd 100644 --- a/src/Nethermind/Nethermind.Blockchain/BlockTree.cs +++ b/src/Nethermind/Nethermind.Blockchain/BlockTree.cs @@ -1433,6 +1433,7 @@ void SetTotalDifficultyDeep(BlockHeader current) public event EventHandler? NewSuggestedBlock; public event EventHandler? NewHeadBlock; + public event EventHandler? OnForkChoiceUpdated; /// /// Can delete a slice of the chain (usually invoked when the chain is corrupted in the DB). @@ -1557,6 +1558,11 @@ public void ForkChoiceUpdated(Hash256? finalizedBlockHash, Hash256? safeBlockHas _metadataDb.Set(MetadataDbKeys.FinalizedBlockHash, Rlp.Encode(FinalizedHash!).Bytes); _metadataDb.Set(MetadataDbKeys.SafeBlockHash, Rlp.Encode(SafeHash!).Bytes); } + + var finalizedNumber = FindHeader(FinalizedHash, BlockTreeLookupOptions.DoNotCreateLevelIfMissing)?.Number; + var safeNumber = FindHeader(safeBlockHash, BlockTreeLookupOptions.DoNotCreateLevelIfMissing)?.Number; + + OnForkChoiceUpdated?.Invoke(this, new(Head?.Number ?? 0, safeNumber ?? 0, finalizedNumber ?? 0)); } } } diff --git a/src/Nethermind/Nethermind.Blockchain/BlockTreeOverlay.cs b/src/Nethermind/Nethermind.Blockchain/BlockTreeOverlay.cs index 6df42379400..90adc2a1f81 100644 --- a/src/Nethermind/Nethermind.Blockchain/BlockTreeOverlay.cs +++ b/src/Nethermind/Nethermind.Blockchain/BlockTreeOverlay.cs @@ -213,6 +213,21 @@ public event EventHandler? OnUpdateMainChain } } + public event EventHandler OnForkChoiceUpdated + { + add + { + _baseTree.OnForkChoiceUpdated += value; + _overlayTree.OnForkChoiceUpdated += value; + } + + remove + { + _baseTree.OnForkChoiceUpdated -= value; + _overlayTree.OnForkChoiceUpdated -= value; + } + } + public int DeleteChainSlice(in long startNumber, long? endNumber = null, bool force = false) => _overlayTree.DeleteChainSlice(startNumber, endNumber, force); diff --git a/src/Nethermind/Nethermind.Blockchain/IBlockTree.cs b/src/Nethermind/Nethermind.Blockchain/IBlockTree.cs index 79f19ed581c..30d2508a5f8 100644 --- a/src/Nethermind/Nethermind.Blockchain/IBlockTree.cs +++ b/src/Nethermind/Nethermind.Blockchain/IBlockTree.cs @@ -170,6 +170,7 @@ AddBlockResult Insert(Block block, BlockTreeInsertBlockOptions insertBlockOption /// the whole branch. /// event EventHandler OnUpdateMainChain; + event EventHandler OnForkChoiceUpdated; int DeleteChainSlice(in long startNumber, long? endNumber = null, bool force = false); @@ -178,5 +179,12 @@ AddBlockResult Insert(Block block, BlockTreeInsertBlockOptions insertBlockOption void UpdateBeaconMainChain(BlockInfo[]? blockInfos, long clearBeaconMainChainStartPoint); void RecalculateTreeLevels(); + + public readonly struct ForkChoice(long head, long safe, long finalized) + { + public readonly long Head => head; + public readonly long Safe => safe; + public readonly long Finalized => finalized; + } } } diff --git a/src/Nethermind/Nethermind.Blockchain/ReadOnlyBlockTree.cs b/src/Nethermind/Nethermind.Blockchain/ReadOnlyBlockTree.cs index 90bfb20a6ac..5cb14c9ed5e 100644 --- a/src/Nethermind/Nethermind.Blockchain/ReadOnlyBlockTree.cs +++ b/src/Nethermind/Nethermind.Blockchain/ReadOnlyBlockTree.cs @@ -149,6 +149,12 @@ public event EventHandler? OnUpdateMainChain remove { } } + event EventHandler IBlockTree.OnForkChoiceUpdated + { + add { } + remove { } + } + public int DeleteChainSlice(in long startNumber, long? endNumber = null, bool force = false) { var bestKnownNumber = BestKnownNumber; diff --git a/src/Nethermind/Nethermind.Consensus/Processing/BlockchainProcessor.cs b/src/Nethermind/Nethermind.Consensus/Processing/BlockchainProcessor.cs index 7ec939b56e0..3bc80b76c1a 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/BlockchainProcessor.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/BlockchainProcessor.cs @@ -63,6 +63,7 @@ public sealed class BlockchainProcessor : IBlockchainProcessor, IBlockProcessing private readonly Stopwatch _stopwatch = new(); public event EventHandler? InvalidBlock; + public event EventHandler? NewProcessingStatistics; /// /// @@ -92,8 +93,12 @@ public BlockchainProcessor( _blockTree.NewHeadBlock += OnNewHeadBlock; _stats = new ProcessingStats(stateReader, _logger); + _stats.NewProcessingStatistics += OnNewProcessingStatistics; } + private void OnNewProcessingStatistics(object? sender, BlockStatistics stats) + => NewProcessingStatistics?.Invoke(sender, stats); + private void OnNewHeadBlock(object? sender, BlockEventArgs e) { _lastProcessedBlock = DateTime.UtcNow; @@ -730,6 +735,7 @@ private bool RunSimpleChecksAheadOfProcessing(Block suggestedBlock, ProcessingOp public void Dispose() { _recoveryComplete = true; + _stats.NewProcessingStatistics -= OnNewProcessingStatistics; _recoveryQueue.Writer.TryComplete(); _blockQueue.Writer.TryComplete(); _loopCancellationSource?.Dispose(); diff --git a/src/Nethermind/Nethermind.Consensus/Processing/IBlockchainProcessor.cs b/src/Nethermind/Nethermind.Consensus/Processing/IBlockchainProcessor.cs index b3703b653f8..a1fd3507f08 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/IBlockchainProcessor.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/IBlockchainProcessor.cs @@ -21,6 +21,7 @@ public interface IBlockchainProcessor : IDisposable bool IsProcessingBlocks(ulong? maxProcessingInterval); event EventHandler InvalidBlock; + event EventHandler NewProcessingStatistics; public class InvalidBlockEventArgs : EventArgs { diff --git a/src/Nethermind/Nethermind.Consensus/Processing/OneTimeProcessor.cs b/src/Nethermind/Nethermind.Consensus/Processing/OneTimeProcessor.cs index 1803b9dd540..1c42ee05023 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/OneTimeProcessor.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/OneTimeProcessor.cs @@ -52,6 +52,7 @@ public bool IsProcessingBlocks(ulong? maxProcessingInterval) public event EventHandler BlockProcessed; public event EventHandler BlockInvalid; public event EventHandler? InvalidBlock; + public event EventHandler NewProcessingStatistics; #pragma warning restore 67 public void Dispose() diff --git a/src/Nethermind/Nethermind.Consensus/Processing/ProcessingStats.cs b/src/Nethermind/Nethermind.Consensus/Processing/ProcessingStats.cs index 0fde5c6baa9..0905bcd879b 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/ProcessingStats.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/ProcessingStats.cs @@ -16,9 +16,24 @@ namespace Nethermind.Consensus.Processing { + public class BlockStatistics + { + public long BlockCount { get; internal set; } + public long BlockFrom { get; internal set; } + public long BlockTo { get; internal set; } + public double ProcessingMs { get; internal set; } + public double SlotMs { get; internal set; } + public double MgasPerSecond { get; internal set; } + public float MinGas { get; internal set; } + public float MedianGas { get; internal set; } + public float AveGas { get; internal set; } + public float MaxGas { get; internal set; } + public long GasLimit { get; internal set; } + } //TODO Consult on disabling of such metrics from configuration internal class ProcessingStats : IThreadPoolWorkItem { + public event EventHandler? NewProcessingStatistics; private readonly IStateReader _stateReader; private readonly ILogger _logger; private readonly Stopwatch _runStopwatch = new(); @@ -195,23 +210,39 @@ void Execute() } } - if (_logger.IsInfo) + long chunkTx = Metrics.Transactions - _lastTotalTx; + long chunkCalls = _currentCallOps - _lastCallOps; + long chunkEmptyCalls = _currentEmptyCalls - _lastEmptyCalls; + long chunkCreates = _currentCreatesOps - _lastCreateOps; + long chunkSload = _currentSLoadOps - _lastSLoadOps; + long chunkSstore = _currentSStoreOps - _lastSStoreOps; + long contractsAnalysed = _currentContractsAnalyzed - _lastContractsAnalyzed; + long cachedContractsUsed = _currentCachedContractsUsed - _lastCachedContractsUsed; + double txps = chunkMicroseconds == 0 ? -1 : chunkTx / chunkMicroseconds * 1_000_000.0; + double bps = chunkMicroseconds == 0 ? -1 : chunkBlocks / chunkMicroseconds * 1_000_000.0; + double chunkMs = (chunkMicroseconds == 0 ? -1 : chunkMicroseconds / 1000.0); + double runMs = (_runMicroseconds == 0 ? -1 : _runMicroseconds / 1000.0); + string blockGas = Evm.Metrics.BlockMinGasPrice != float.MaxValue ? $"⛽ Gas gwei: {Evm.Metrics.BlockMinGasPrice:N2} .. {whiteText}{Math.Max(Evm.Metrics.BlockMinGasPrice, Evm.Metrics.BlockEstMedianGasPrice):N2}{resetColor} ({Evm.Metrics.BlockAveGasPrice:N2}) .. {Evm.Metrics.BlockMaxGasPrice:N2}" : ""; + string mgasColor = whiteText; + + NewProcessingStatistics?.Invoke(this, new BlockStatistics() { - long chunkTx = Metrics.Transactions - _lastTotalTx; - long chunkCalls = _currentCallOps - _lastCallOps; - long chunkEmptyCalls = _currentEmptyCalls - _lastEmptyCalls; - long chunkCreates = _currentCreatesOps - _lastCreateOps; - long chunkSload = _currentSLoadOps - _lastSLoadOps; - long chunkSstore = _currentSStoreOps - _lastSStoreOps; - long contractsAnalysed = _currentContractsAnalyzed - _lastContractsAnalyzed; - long cachedContractsUsed = _currentCachedContractsUsed - _lastCachedContractsUsed; - double txps = chunkMicroseconds == 0 ? -1 : chunkTx / chunkMicroseconds * 1_000_000.0; - double bps = chunkMicroseconds == 0 ? -1 : chunkBlocks / chunkMicroseconds * 1_000_000.0; - double chunkMs = (chunkMicroseconds == 0 ? -1 : chunkMicroseconds / 1000.0); - double runMs = (_runMicroseconds == 0 ? -1 : _runMicroseconds / 1000.0); - string blockGas = Evm.Metrics.BlockMinGasPrice != float.MaxValue ? $"⛽ Gas gwei: {Evm.Metrics.BlockMinGasPrice:N2} .. {whiteText}{Math.Max(Evm.Metrics.BlockMinGasPrice, Evm.Metrics.BlockEstMedianGasPrice):N2}{resetColor} ({Evm.Metrics.BlockAveGasPrice:N2}) .. {Evm.Metrics.BlockMaxGasPrice:N2}" : ""; - string mgasColor = whiteText; + BlockCount = chunkBlocks, + BlockFrom = block.Number - chunkBlocks + 1, + BlockTo = block.Number, + + ProcessingMs = chunkMs, + SlotMs = runMs, + MgasPerSecond = mgasPerSecond, + MinGas = Evm.Metrics.BlockMinGasPrice, + MedianGas = Math.Max(Evm.Metrics.BlockMinGasPrice, Evm.Metrics.BlockEstMedianGasPrice), + AveGas = Evm.Metrics.BlockAveGasPrice, + MaxGas = Evm.Metrics.BlockMaxGasPrice, + GasLimit = block.GasLimit + }); + if (_logger.IsInfo) + { if (chunkBlocks > 1) { _logger.Info($"Processed {block.Number - chunkBlocks + 1,10}...{block.Number,9} | {chunkMs,10:N1} ms | slot {runMs,11:N0} ms |{blockGas}"); diff --git a/src/Nethermind/Nethermind.Runner.Test/StandardTests.cs b/src/Nethermind/Nethermind.Runner.Test/StandardTests.cs index 6724ebee562..616cba73ec4 100644 --- a/src/Nethermind/Nethermind.Runner.Test/StandardTests.cs +++ b/src/Nethermind/Nethermind.Runner.Test/StandardTests.cs @@ -19,7 +19,7 @@ public void All_json_rpc_methods_are_documented() [Test] public void All_metrics_are_described() { - Monitoring.Test.MetricsTests.ValidateMetricsDescriptions(); + Nethermind.Monitoring.Test.MetricsTests.ValidateMetricsDescriptions(); } [Test] diff --git a/src/Nethermind/Nethermind.Runner/ConsoleHelpers.cs b/src/Nethermind/Nethermind.Runner/ConsoleHelpers.cs index 5ff5676fffd..8c55c2b1fc8 100644 --- a/src/Nethermind/Nethermind.Runner/ConsoleHelpers.cs +++ b/src/Nethermind/Nethermind.Runner/ConsoleHelpers.cs @@ -2,12 +2,19 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Collections.Generic; +using System.IO; using System.Runtime.InteropServices; +using System.Text; namespace Nethermind.Runner; public static class ConsoleHelpers { + private static LineInterceptingTextWriter _interceptingWriter; + public static event EventHandler? LineWritten; + public static string[] GetRecentMessages() => _interceptingWriter.GetRecentMessages(); + public static void EnableConsoleColorOutput() { const int STD_OUTPUT_HANDLE = -11; @@ -15,6 +22,19 @@ public static void EnableConsoleColorOutput() Console.OutputEncoding = System.Text.Encoding.UTF8; + // Capture original out + TextWriter originalOut = Console.Out; + + // Create our intercepting writer + _interceptingWriter = new LineInterceptingTextWriter(originalOut); + _interceptingWriter.LineWritten += (sender, line) => + { + LineWritten?.Invoke(sender, line); + }; + + // Redirect Console.Out + Console.SetOut(_interceptingWriter); + if (!OperatingSystem.IsWindowsVersionAtLeast(10)) return; @@ -41,3 +61,114 @@ public static void EnableConsoleColorOutput() [DllImport("kernel32.dll")] private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); } + + +public sealed class LineInterceptingTextWriter : TextWriter +{ + // Event raised every time a full line ending with Environment.NewLine is written + public event EventHandler? LineWritten; + + // The "real" underlying writer (i.e., the original Console.Out) + private readonly TextWriter _underlyingWriter; + + // Buffer used to accumulate written data until we detect a new line + private readonly StringBuilder _buffer; + + public LineInterceptingTextWriter(TextWriter underlyingWriter) + { + _underlyingWriter = underlyingWriter ?? throw new ArgumentNullException(nameof(underlyingWriter)); + _buffer = new StringBuilder(); + } + + // You must override Encoding, even if just forwarding + public override Encoding Encoding => _underlyingWriter.Encoding; + + // Overriding WriteLine(string) is handy for direct calls to Console.WriteLine(...). + // However, you also want to handle the general case in Write(string). + public override void WriteLine(string? value) + { + Write(value); + Write(Environment.NewLine); + } + + public override void Write(string? value) + { + if (value == null) + { + return; + } + + // Append to the buffer + _buffer.Append(value); + + // Pass the data along to the underlying writer + _underlyingWriter.Write(value); + + // Check if we can extract lines from the buffer + CheckForLines(); + } + + public override void Write(char value) + { + _buffer.Append(value); + _underlyingWriter.Write(value); + CheckForLines(); + } + + public override void Flush() + { + base.Flush(); + _underlyingWriter.Flush(); + } + + private void CheckForLines() + { + // Environment.NewLine might be "\r\n" or "\n" depending on platform + // so let's find each occurrence and split it off + string newLine = Environment.NewLine; + + while (true) + { + // Find the next index of the new line + int newLinePos = _buffer.ToString().IndexOf(newLine, StringComparison.Ordinal); + + // If there's no complete new line, break + if (newLinePos < 0) + { + break; + } + + // Extract the line up to the new line + string line = _buffer.ToString(0, newLinePos); + + // Remove that portion (including the new line) from the buffer + _buffer.Remove(0, newLinePos + newLine.Length); + + // Raise the event + OnLineWritten(line); + } + } + + public string[] GetRecentMessages() + { + lock (_recentMessages) + { + return _recentMessages.ToArray(); + } + } + + Queue _recentMessages = new(capacity: 100); + private void OnLineWritten(string line) + { + lock (_recentMessages) + { + if (_recentMessages.Count > 100) + { + _recentMessages.Dequeue(); + } + _recentMessages.Enqueue(line); + } + // Raise the event, if subscribed + LineWritten?.Invoke(this, line); + } +} diff --git a/src/Nethermind/Nethermind.Runner/Ethereum/JsonRpcRunner.cs b/src/Nethermind/Nethermind.Runner/Ethereum/JsonRpcRunner.cs index 9dde30d01e3..4c29613fe59 100644 --- a/src/Nethermind/Nethermind.Runner/Ethereum/JsonRpcRunner.cs +++ b/src/Nethermind/Nethermind.Runner/Ethereum/JsonRpcRunner.cs @@ -76,6 +76,7 @@ public async Task Start(CancellationToken cancellationToken) s.AddSingleton(_jsonRpcUrlCollection); s.AddSingleton(_webSocketsManager); s.AddSingleton(_rpcAuthentication); + s.AddSingleton(_api); foreach (var plugin in _api.Plugins.OfType()) { plugin.AddServices(s); diff --git a/src/Nethermind/Nethermind.Runner/JsonRpc/Startup.cs b/src/Nethermind/Nethermind.Runner/JsonRpc/Startup.cs index d1d7afdbb2c..1699c26e75c 100644 --- a/src/Nethermind/Nethermind.Runner/JsonRpc/Startup.cs +++ b/src/Nethermind/Nethermind.Runner/JsonRpc/Startup.cs @@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Nethermind.Api; @@ -135,10 +136,14 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IJsonRpc { if (logger.IsError) logger.Error("Unable to initialize health checks. Check if you have Nethermind.HealthChecks.dll in your plugins folder.", e); } + + endpoints.MapDataFeeds(app.ApplicationServices.GetRequiredService()); } }); - app.Run(async ctx => + app.MapWhen( + (ctx) => ctx.Request.ContentType?.Contains("application/json") ?? false, + builder => builder.Run(async ctx => { var method = ctx.Request.Method; if (method is not "POST" and not "GET") @@ -167,7 +172,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IJsonRpc } } - if (method == "GET") + if (method == "GET" && !(ctx.Request.Headers.Accept[0].Contains("text/html", StringComparison.Ordinal))) { await ctx.Response.WriteAsync("Nethermind JSON RPC"); } @@ -294,7 +299,13 @@ async Task PushErrorResponse(int statusCode, int errorCode, string message) await jsonSerializer.SerializeAsync(ctx.Response.BodyWriter, response); await ctx.Response.CompleteAsync(); } - }); + })); + + if (healthChecksConfig.Enabled) + { + app.UseDefaultFiles(); + app.UseStaticFiles(); + } } /// diff --git a/src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs b/src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs new file mode 100644 index 00000000000..c1f47ada43c --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs @@ -0,0 +1,245 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Diagnostics; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +using Nethermind.Api; +using Nethermind.Blockchain; +using Nethermind.Consensus.Processing; +using Nethermind.Runner.Monitoring.TransactionPool; +using Nethermind.TxPool; + +namespace Nethermind.Runner.Monitoring; + +public class DataFeed +{ + public static long StartTime { get; set; } + + private readonly ITxPool _txPool; + private readonly IBlockTree _blockTree; + + public DataFeed(INethermindApi api) + { + _txPool = api.TxPool; + _blockTree = api.BlockTree; + + ArgumentNullException.ThrowIfNull(_txPool); + ArgumentNullException.ThrowIfNull(_blockTree); + + api.BlockchainProcessor.NewProcessingStatistics += OnNewProcessingStatistics; + _blockTree.OnForkChoiceUpdated += OnForkChoiceUpdated; + ConsoleHelpers.LineWritten += OnConsoleLineWritten; + _ = StartTxFlowRefresh(); + _ = SystemStatsRefresh(); + } + + public async Task ProcessingFeed(HttpContext ctx, CancellationToken ct) + { + ctx.Response.ContentType = "text/event-stream"; + + await ctx.Response.WriteAsync($"event: nodeData\n", cancellationToken: ct); + await ctx.Response.WriteAsync($"data: ", cancellationToken: ct); + await ctx.Response.Body.WriteAsync(GetNodeData(), cancellationToken: ct); + await ctx.Response.WriteAsync($"\n\n", cancellationToken: ct); + + await ctx.Response.WriteAsync($"event: txNodes\n", cancellationToken: ct); + await ctx.Response.WriteAsync($"data: ", cancellationToken: ct); + await ctx.Response.Body.WriteAsync(TxPoolFlow.NodeJson, cancellationToken: ct); + await ctx.Response.WriteAsync($"\n\n", cancellationToken: ct); + + await ctx.Response.WriteAsync($"event: log\n", cancellationToken: ct); + await ctx.Response.WriteAsync($"data: ", cancellationToken: ct); + await ctx.Response.Body.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(ConsoleHelpers.GetRecentMessages(), JsonSerializerOptions.Web), cancellationToken: ct); + await ctx.Response.WriteAsync($"\n\n", cancellationToken: ct); + + var channel = Channel.CreateUnbounded(); + + InitializeChannelSubscriptions(channel, ct); + + await foreach (ChannelEntry entry in channel.Reader.ReadAllAsync(ct)) + { + await ctx.Response.WriteAsync($"event: {entry.Type}\n", cancellationToken: ct); + await ctx.Response.WriteAsync($"data: ", cancellationToken: ct); + await ctx.Response.Body.WriteAsync(entry.Data, cancellationToken: ct); + await ctx.Response.WriteAsync($"\n\n", cancellationToken: ct); + + if (channel.Reader.Count == 0) + { + await ctx.Response.Body.FlushAsync(cancellationToken: ct); + } + } + } + + enum EntryType + { + nodeData, + txNodes, + log, + processed, + txLinks, + forkChoice, + system + } + + class ChannelEntry + { + public EntryType Type { get; set; } + public byte[] Data { get; set; } + } + private static async Task ChannelSubscribe(EntryType type, Func> nextTask, Channel channel, CancellationToken ct) + { + Task task = nextTask(); + + while (!ct.IsCancellationRequested) + { + byte[] data = await task; + task = nextTask(); + await channel.Writer.WriteAsync(new ChannelEntry { Type = type, Data = data }, ct); + } + } + + private void InitializeChannelSubscriptions(Channel channel, CancellationToken ct) + { + _ = ChannelSubscribe(EntryType.processed, () => _processing.Task, channel, ct); + _ = ChannelSubscribe(EntryType.log, () => _log.Task, channel, ct); + _ = ChannelSubscribe(EntryType.forkChoice, () => _forkChoice.Task, channel, ct); + _ = ChannelSubscribe(EntryType.txLinks, () => _txFlow.Task, channel, ct); + _ = ChannelSubscribe(EntryType.system, () => _systemStats.Task, channel, ct); + } + + private byte[] GetNodeData() + { + return JsonSerializer.SerializeToUtf8Bytes( + new NethermindNodeData(uptime: Environment.TickCount64 - DataFeed.StartTime), + JsonSerializerOptions.Web); + } + + TaskCompletionSource _txFlow = new(); + private async Task StartTxFlowRefresh() + { + while (true) + { + byte[] data = await GetTxFlowTask(delayMs: 1000); + + var txFlow = _txFlow; + _txFlow = new TaskCompletionSource(); + txFlow.TrySetResult(data); + } + } + + Environment.ProcessCpuUsage _lastCpuUsage; + long _lastTimeStamp; + TaskCompletionSource _systemStats = new(); + private async Task SystemStatsRefresh() + { + _lastCpuUsage = Environment.CpuUsage; + _lastTimeStamp = Stopwatch.GetTimestamp(); + while (true) + { + var data = await GetStatsTask(delayMs: 1000); + var systemStats = _systemStats; + _systemStats = new(); + systemStats.TrySetResult(data); + } + } + + private async Task GetStatsTask(int delayMs) + { + await Task.Delay(delayMs); + + Environment.ProcessCpuUsage cpuUsage = Environment.CpuUsage; + long timeStamp = Stopwatch.GetTimestamp(); + + TimeSpan elapsed = Stopwatch.GetElapsedTime(_lastTimeStamp, timeStamp); + + var stats = new SystemStats + { + UserPercent = ((cpuUsage.UserTime - _lastCpuUsage.UserTime).TotalMicroseconds / elapsed.TotalMicroseconds) / Environment.ProcessorCount, + PrivilegedPercent = ((cpuUsage.PrivilegedTime - _lastCpuUsage.PrivilegedTime).TotalMicroseconds / elapsed.TotalMicroseconds) / Environment.ProcessorCount, + WorkingSet = Environment.WorkingSet, + Uptime = Environment.TickCount64 - DataFeed.StartTime + }; + _lastTimeStamp = timeStamp; + _lastCpuUsage = cpuUsage; + + return JsonSerializer.SerializeToUtf8Bytes(stats, JsonSerializerOptions.Web); + } + + private async Task GetTxFlowTask(int delayMs) + { + await Task.Delay(delayMs); + return JsonSerializer.SerializeToUtf8Bytes(new TxPoolFlow( + TxPool.Metrics.PendingTransactionsReceived, + TxPool.Metrics.PendingTransactionsNotSupportedTxType, + TxPool.Metrics.PendingTransactionsSizeTooLarge, + TxPool.Metrics.PendingTransactionsGasLimitTooHigh, + TxPool.Metrics.PendingTransactionsTooLowPriorityFee, + TxPool.Metrics.PendingTransactionsTooLowFee, + TxPool.Metrics.PendingTransactionsMalformed, + TxPool.Metrics.PendingTransactionsNullHash, + TxPool.Metrics.PendingTransactionsKnown, + TxPool.Metrics.PendingTransactionsUnresolvableSender, + TxPool.Metrics.PendingTransactionsConflictingTxType, + TxPool.Metrics.PendingTransactionsNonceTooFarInFuture, + TxPool.Metrics.PendingTransactionsZeroBalance, + TxPool.Metrics.PendingTransactionsBalanceBelowValue, + TxPool.Metrics.PendingTransactionsTooLowBalance, + TxPool.Metrics.PendingTransactionsLowNonce, + TxPool.Metrics.PendingTransactionsNonceGap, + TxPool.Metrics.PendingTransactionsPassedFiltersButCannotReplace, + TxPool.Metrics.PendingTransactionsPassedFiltersButCannotCompeteOnFees, + TxPool.Metrics.PendingTransactionsEvicted, + TxPool.Metrics.TransactionsSourcedPrivateOrderflow, + TxPool.Metrics.TransactionsSourcedMemPool, + TxPool.Metrics.TransactionsReorged + ) + { + PooledBlobTx = _txPool.GetPendingBlobTransactionsCount(), + PooledTx = _txPool.GetPendingTransactionsCount(), + HashesReceived = TxPool.Metrics.PendingTransactionsHashesReceived + }, + JsonSerializerOptions.Web); + } + + TaskCompletionSource _processing = new(); + private void OnNewProcessingStatistics(object? sender, BlockStatistics stats) + { + TaskCompletionSource processing = _processing; + _processing = new TaskCompletionSource(); + + processing.TrySetResult(JsonSerializer.SerializeToUtf8Bytes(stats, JsonSerializerOptions.Web)); + } + + TaskCompletionSource _forkChoice = new(); + private void OnForkChoiceUpdated(object? sender, IBlockTree.ForkChoice choice) + { + TaskCompletionSource forkChoice = _forkChoice; + _forkChoice = new TaskCompletionSource(); + + forkChoice.TrySetResult(JsonSerializer.SerializeToUtf8Bytes(choice, JsonSerializerOptions.Web)); + } + + TaskCompletionSource _log = new(); + private void OnConsoleLineWritten(object? sender, string logLine) + { + TaskCompletionSource log = _log; + _log = new TaskCompletionSource(); + + log.TrySetResult(JsonSerializer.SerializeToUtf8Bytes(new[] { logLine }, JsonSerializerOptions.Web)); + } +} + +internal class SystemStats +{ + public double UserPercent { get; set; } + public double PrivilegedPercent { get; set; } + public long Uptime { get; set; } + public long WorkingSet { get; internal set; } +} diff --git a/src/Nethermind/Nethermind.Runner/Monitoring/DataFeedExtensions.cs b/src/Nethermind/Nethermind.Runner/Monitoring/DataFeedExtensions.cs new file mode 100644 index 00000000000..77909e52517 --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/Monitoring/DataFeedExtensions.cs @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Nethermind.Api; +using Nethermind.Runner.Monitoring; +using Nethermind.TxPool; + +namespace Nethermind.Runner; + +public static class DataFeedExtensions +{ + private static DataFeed _dataFeed; + + public static void MapDataFeeds(this IEndpointRouteBuilder endpoints, INethermindApi api) + { + ArgumentNullException.ThrowIfNull(endpoints); + ArgumentNullException.ThrowIfNull(api); + + _dataFeed = new DataFeed(api); + + endpoints.MapGet("/data/events", _dataFeed.ProcessingFeed); + } +} diff --git a/src/Nethermind/Nethermind.Runner/Monitoring/NethermindNodeData.cs b/src/Nethermind/Nethermind.Runner/Monitoring/NethermindNodeData.cs new file mode 100644 index 00000000000..f26550dc72b --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/Monitoring/NethermindNodeData.cs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Runner.Monitoring.TransactionPool; + +namespace Nethermind.Runner.Monitoring; + +internal class NethermindNodeData(long uptime) +{ + public long Uptime => uptime; + public string Instance => ProductInfo.Instance; + public string Network => ProductInfo.Network; + public string SyncType => ProductInfo.SyncType; + public string PruningMode => ProductInfo.PruningMode; + public string Version => ProductInfo.Version; + public string Commit => ProductInfo.Commit; + public string Runtime => ProductInfo.Runtime; +} diff --git a/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/Link.cs b/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/Link.cs new file mode 100644 index 00000000000..2e986fd39f6 --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/Link.cs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Runner.Monitoring.TransactionPool; + +internal class Link(string source, string target, long value) +{ + public string Source { get; } = source; + public string Target { get; } = target; + public long Value { get; } = value; +} diff --git a/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/Node.cs b/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/Node.cs new file mode 100644 index 00000000000..abe018e774b --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/Node.cs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; + +namespace Nethermind.Runner.Monitoring.TransactionPool; + +internal class Node(string name, bool inclusion = false) +{ + public string Name { get; set; } = name ?? throw new ArgumentNullException(nameof(name)); + public bool Inclusion { get; set; } = inclusion; +} diff --git a/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/TxPoolFlow.cs b/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/TxPoolFlow.cs new file mode 100644 index 00000000000..1924a4b999b --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/TxPoolFlow.cs @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Serialization.Json; + +using System; +using System.Text.Json; + +namespace Nethermind.Runner.Monitoring.TransactionPool; + +internal class TxPoolFlow +{ + public static ReadOnlyMemory NodeJson => _nodeJson; + private static readonly byte[] _nodeJson = JsonSerializer.SerializeToUtf8Bytes( + new Node[] { + new (TxPoolStages.P2P, inclusion: true), + new(TxPoolStages.ReceivedTxs, inclusion: true), + new(TxPoolStages.NotSupportedTxType), + new(TxPoolStages.TxTooLarge), + new(TxPoolStages.GasLimitTooHigh), + new(TxPoolStages.TooLowPriorityFee), + new(TxPoolStages.TooLowFee), + new(TxPoolStages.Malformed), + new(TxPoolStages.NullHash), + new(TxPoolStages.Duplicate), + new(TxPoolStages.UnknownSender), + new(TxPoolStages.ConflictingTxType), + new(TxPoolStages.NonceTooFarInFuture), + new(TxPoolStages.StateValidation, inclusion: true), + new(TxPoolStages.ZeroBalance), + new(TxPoolStages.BalanceLtTxValue), + new(TxPoolStages.BalanceTooLow), + new(TxPoolStages.NonceUsed), + new(TxPoolStages.NoncesSkipped), + new(TxPoolStages.ValidationSucceeded, inclusion: true), + new(TxPoolStages.FailedReplacement), + new(TxPoolStages.CannotCompete), + new(TxPoolStages.TransactionPool, inclusion: true), + new(TxPoolStages.Evicted), + new(TxPoolStages.PrivateOrderFlow, inclusion: true), + new(TxPoolStages.AddedToBlock, inclusion: true), + new(TxPoolStages.ReorgedOut), + new(TxPoolStages.ReorgedIn, inclusion: true), + }, JsonSerializerOptions.Web); + + public Link[] Links { get; } + + public int PooledBlobTx { get; init; } + public int PooledTx { get; init; } + public long HashesReceived { get; internal set; } + + // Constructor that takes the metrics (TxPoolStages are fixed). + public TxPoolFlow( + long pendingTransactionsReceived, + long pendingTransactionsNotSupportedTxType, + long pendingTransactionsSizeTooLarge, + long pendingTransactionsGasLimitTooHigh, + long pendingTransactionsTooLowPriorityFee, + long pendingTransactionsTooLowFee, + long pendingTransactionsMalformed, + long pendingTransactionsNullHash, + long pendingTransactionsKnown, + long pendingTransactionsUnresolvableSender, + long pendingTransactionsConflictingTxType, + long pendingTransactionsNonceTooFarInFuture, + long pendingTransactionsZeroBalance, + long pendingTransactionsBalanceBelowValue, + long pendingTransactionsTooLowBalance, + long pendingTransactionsLowNonce, + long pendingTransactionsNonceGap, + long pendingTransactionsPassedFiltersButCannotReplace, + long pendingTransactionsPassedFiltersButCannotCompeteOnFees, + long pendingTransactionsEvicted, + long privateOrderFlow, + long memPoolFlow, + long reorged + ) + { + + var stateValidation = pendingTransactionsReceived + - pendingTransactionsNotSupportedTxType + - pendingTransactionsSizeTooLarge + - pendingTransactionsGasLimitTooHigh + - pendingTransactionsTooLowPriorityFee + - pendingTransactionsTooLowFee + - pendingTransactionsMalformed + - pendingTransactionsNullHash + - pendingTransactionsKnown + - pendingTransactionsUnresolvableSender + - pendingTransactionsConflictingTxType + - pendingTransactionsNonceTooFarInFuture; + var validationSuccess = stateValidation + - pendingTransactionsZeroBalance + - pendingTransactionsBalanceBelowValue + - pendingTransactionsTooLowBalance + - pendingTransactionsLowNonce + - pendingTransactionsNonceGap; + var addedToPool = validationSuccess + - pendingTransactionsPassedFiltersButCannotReplace + - pendingTransactionsPassedFiltersButCannotCompeteOnFees; + + Links = new[] + { + new Link(TxPoolStages.P2P, TxPoolStages.ReceivedTxs, pendingTransactionsReceived), + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.NotSupportedTxType, pendingTransactionsNotSupportedTxType), + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.TxTooLarge, pendingTransactionsSizeTooLarge), + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.GasLimitTooHigh, pendingTransactionsGasLimitTooHigh), + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.TooLowPriorityFee, pendingTransactionsTooLowPriorityFee), + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.TooLowFee, pendingTransactionsTooLowFee), + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.Malformed, pendingTransactionsMalformed), + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.NullHash, pendingTransactionsNullHash), + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.Duplicate, pendingTransactionsKnown), + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.UnknownSender, pendingTransactionsUnresolvableSender), + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.ConflictingTxType, pendingTransactionsConflictingTxType), + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.NonceTooFarInFuture, pendingTransactionsNonceTooFarInFuture), + + new Link(TxPoolStages.ReceivedTxs, TxPoolStages.StateValidation, stateValidation), + + new Link(TxPoolStages.StateValidation, TxPoolStages.ZeroBalance, pendingTransactionsZeroBalance), + new Link(TxPoolStages.StateValidation, TxPoolStages.BalanceLtTxValue, pendingTransactionsBalanceBelowValue), + new Link(TxPoolStages.StateValidation, TxPoolStages.BalanceTooLow, pendingTransactionsTooLowBalance), + new Link(TxPoolStages.StateValidation, TxPoolStages.NonceUsed, pendingTransactionsLowNonce), + new Link(TxPoolStages.StateValidation, TxPoolStages.NoncesSkipped, pendingTransactionsNonceGap), + + new Link(TxPoolStages.StateValidation, TxPoolStages.ValidationSucceeded, validationSuccess), + + new Link(TxPoolStages.ValidationSucceeded, TxPoolStages.FailedReplacement, + pendingTransactionsPassedFiltersButCannotReplace), + new Link(TxPoolStages.ValidationSucceeded, TxPoolStages.CannotCompete, + pendingTransactionsPassedFiltersButCannotCompeteOnFees), + + new Link(TxPoolStages.ValidationSucceeded, TxPoolStages.TransactionPool, addedToPool), + new Link(TxPoolStages.TransactionPool, TxPoolStages.Evicted, pendingTransactionsEvicted), + new Link(TxPoolStages.TransactionPool, TxPoolStages.AddedToBlock, privateOrderFlow), + new Link(TxPoolStages.PrivateOrderFlow, TxPoolStages.AddedToBlock, memPoolFlow), + new Link(TxPoolStages.AddedToBlock, TxPoolStages.ReorgedOut, reorged), + new Link(TxPoolStages.ReorgedIn, TxPoolStages.ReceivedTxs, reorged) + }; + } +} diff --git a/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/TxPoolStages.cs b/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/TxPoolStages.cs new file mode 100644 index 00000000000..8db23c1a474 --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/Monitoring/TxPool/TxPoolStages.cs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Runner.Monitoring.TransactionPool; + +internal static class TxPoolStages +{ + public const string P2P = "P2P Network"; + public const string ReceivedTxs = "Received Txs"; + public const string NotSupportedTxType = "Not Supported Tx Type"; + public const string TxTooLarge = "Tx Too Large"; + public const string GasLimitTooHigh = "Gas Limit Too High"; + public const string TooLowPriorityFee = "Low Priority Fee"; + public const string TooLowFee = "Too Low Fee"; + public const string Malformed = "Malformed"; + public const string NullHash = "NullHash"; + public const string Duplicate = "Duplicate"; + public const string UnknownSender = "Unknown Sender"; + public const string ConflictingTxType = "Conflicting Tx Type"; + public const string NonceTooFarInFuture = "Nonce Too Far In Future"; + public const string StateValidation = "State Validation"; + public const string ZeroBalance = "Zero Balance"; + public const string BalanceLtTxValue = "Balance < Tx.Value"; + public const string BalanceTooLow = "Balance Too Low"; + public const string NonceUsed = "Nonce Used"; + public const string NoncesSkipped = "Nonces Skipped"; + public const string ValidationSucceeded = "Validation Succeeded"; + public const string FailedReplacement = "Failed Replacement"; + public const string CannotCompete = "Cannot Compete"; + public const string TransactionPool = "Tx Pool"; + public const string Evicted = "Evicted"; + public const string PrivateOrderFlow = "Private Order Flow"; + public const string AddedToBlock = "Added To Block"; + public const string ReorgedOut = "Reorged Out"; + public const string ReorgedIn = "Reorged In"; +} diff --git a/src/Nethermind/Nethermind.Runner/NLog.config b/src/Nethermind/Nethermind.Runner/NLog.config index 11993b49c13..94684061a15 100644 --- a/src/Nethermind/Nethermind.Runner/NLog.config +++ b/src/Nethermind/Nethermind.Runner/NLog.config @@ -23,6 +23,7 @@ diff --git a/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj b/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj index 2cc6fa3b095..b10e510a1a8 100644 --- a/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj +++ b/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj @@ -83,11 +83,30 @@ true + + + + + PreserveNewest + Always + + + + + + + + + + + + + diff --git a/src/Nethermind/Nethermind.Runner/Program.cs b/src/Nethermind/Nethermind.Runner/Program.cs index f98f3503dc6..02dfd69a1b9 100644 --- a/src/Nethermind/Nethermind.Runner/Program.cs +++ b/src/Nethermind/Nethermind.Runner/Program.cs @@ -36,6 +36,7 @@ using Nethermind.Runner.Ethereum; using Nethermind.Runner.Ethereum.Api; using Nethermind.Runner.Logging; +using Nethermind.Runner.Monitoring; using Nethermind.Seq.Config; using Nethermind.Serialization.Json; using Nethermind.UPnP.Plugin; @@ -44,6 +45,7 @@ using ILogger = Nethermind.Logging.ILogger; using NullLogger = Nethermind.Logging.NullLogger; +DataFeed.StartTime = Environment.TickCount64; Console.Title = ProductInfo.Name; // Increase regex cache size as more added in log coloring matches Regex.CacheSize = 128; diff --git a/src/Nethermind/Nethermind.Runner/package.json b/src/Nethermind/Nethermind.Runner/package.json new file mode 100644 index 00000000000..61806816180 --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/package.json @@ -0,0 +1,24 @@ +{ + "name": "nethermind-stats", + "version": "1.0.0", + "description": "", + "keywords": [], + "author": "", + "license": "LGPL-3.0-only", + "private": true, + "scripts": { + "build:dev": "yarn && esbuild scripts/app.ts --sourcemap=inline --bundle --outfile=wwwroot/js/bundle.js", + "build:release": "yarn && esbuild scripts/app.ts --minify --bundle --outfile=wwwroot/js/bundle.js" + }, + "devDependencies": { + "@types/d3": "7.4.3", + "esbuild": "0.24.2", + "typescript": "5.7.3", + "yarn": "1.22.22" + }, + "dependencies": { + "d3": "7.9.0", + "d3-sankey": "0.12.3", + "ansi-to-html": "0.7.2" + } +} diff --git a/src/Nethermind/Nethermind.Runner/scripts/app.ts b/src/Nethermind/Nethermind.Runner/scripts/app.ts new file mode 100644 index 00000000000..0b65a092a8f --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/scripts/app.ts @@ -0,0 +1,262 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +import * as d3 from 'd3'; +import Convert = require('ansi-to-html'); +import { formatDuration } from './format'; +import { sparkline, Datum } from './sparkline'; +import { NodeData, INode, TxPool, Processed, ForkChoice, System } from './types'; +import { TxPoolFlow } from './txPoolFlow'; + +// Grab DOM elements +const txPoolValue = document.getElementById('txPoolValue') as HTMLElement; +const blobTxPoolValue = document.getElementById('blobTxPoolValue') as HTMLElement; +const totalValue = document.getElementById('totalValue') as HTMLElement; + +const blockTpsValue = document.getElementById('blockTpsValue') as HTMLElement; +const receivedTpsValue = document.getElementById('receivedTpsValue') as HTMLElement; +const txPoolTpsValue = document.getElementById('txPoolTpsValue') as HTMLElement; +const duplicateTpsValue = document.getElementById('duplicateTpsValue') as HTMLElement; +const hashesReceivedTpsValue = document.getElementById('hashesReceivedTpsValue') as HTMLElement; +const version = document.getElementById('version') as HTMLElement; + +const upTime = document.getElementById('upTime') as HTMLElement; +const network = document.getElementById('network') as HTMLElement; +const nodeLog = document.getElementById('nodeLog') as HTMLElement; +const headBlock = document.getElementById('headBlock') as HTMLElement; +const safeBlock = document.getElementById('safeBlock') as HTMLElement; +const finalizedBlock = document.getElementById('finalizedBlock') as HTMLElement; +const safeBlockDelta = document.getElementById('safeBlockDelta') as HTMLElement; +const finalizedBlockDelta = document.getElementById('finalizedBlockDelta') as HTMLElement; + +const sparkCpu = document.getElementById('sparkCpu') as HTMLElement; +const cpuTime = document.getElementById('cpuTime') as HTMLElement; +const maxCpuTime = document.getElementById('maxCpuTime') as HTMLElement; + +const sparkMemory = document.getElementById('sparkMemory') as HTMLElement; +const memory = document.getElementById('memory') as HTMLElement; +const maxMemory = document.getElementById('maxMemory') as HTMLElement; + +const minGas = document.getElementById('minGas') as HTMLElement; +const medianGas = document.getElementById('medianGas') as HTMLElement; +const aveGas = document.getElementById('aveGas') as HTMLElement; +const maxGas = document.getElementById('maxGas') as HTMLElement; +const gasLimit = document.getElementById('gasLimit') as HTMLElement; +const gasLimitDelta = document.getElementById('gasLimitDelta') as HTMLElement; + +const ansiConvert = new Convert(); + +// We reuse these arrays for the sparkline. The length = 60 means we store 60 historical points. +let seriesHashes: Datum[] = []; +let seriesReceived: Datum[] = []; +let seriesTxPool: Datum[] = []; +let seriesBlock: Datum[] = []; +let seriesDuplicate: Datum[] = []; + +let seriesTotalCpu: Datum[] = []; +let seriesMemory: Datum[] = []; + +// Keep track of last values so we can compute TPS +let lastReceived = 0; +let lastTxPool = 0; +let lastBlock = 0; +let lastDuplicate = 0; +let lastHashesReceived = 0; +let lastNow = 0; + +function updateText(element: HTMLElement, value: string): void { + if (element.innerText !== value) { + // Don't update the DOM if the value is the same + element.innerText = value; + } +} + +// Initialize the Sankey flow +const txPoolFlow = new TxPoolFlow('#txPoolFlow'); + +// Number format +const format = d3.format(',.0f'); +const formatDec = d3.format(',.1f'); + +let txPoolNodes: INode[] = null; +/** + * Main function to start polling data and updating the UI. + */ +function updateTxPool(txPool: TxPool) { + + if (!txPoolNodes) { + return; + } + // Update Sankey + txPoolFlow.update(txPoolNodes, txPool); + + // Update numeric indicators + updateText(txPoolValue, d3.format(',.0f')(txPool.pooledTx)); + updateText(blobTxPoolValue, d3.format(',.0f')(txPool.pooledBlobTx)); + updateText(totalValue, d3.format(',.0f')(txPool.pooledTx + txPool.pooledBlobTx)); + + // Summarize link flows to compute TPS + let currentReceived = 0; + let currentTxPool = 0; + let currentBlock = 0; + let currentDuplicate = 0; + + for (const link of txPool.links) { + if (link.target === 'Received Txs') { + currentReceived += link.value; + } + if (link.target === 'Tx Pool') { + currentTxPool += link.value; + } + if (link.target === 'Added To Block') { + currentBlock += link.value; + } + if (link.target === 'Duplicate') { + currentDuplicate += link.value; + } + } + const currentHashesReceived = txPool.hashesReceived; + const nowMs = performance.now(); + const currentNow = nowMs / 1000; + + if (lastNow !== 0) { + const diff = currentNow - lastNow; + + // Update the sparkline for each type + sparkline(document.getElementById('sparkHashesTps') as HTMLElement, + seriesHashes, { t: nowMs, v: currentHashesReceived - lastHashesReceived }); + sparkline(document.getElementById('sparkReceivedTps') as HTMLElement, + seriesReceived, { t: nowMs, v: currentReceived - lastReceived }); + sparkline(document.getElementById('sparkDuplicateTps') as HTMLElement, + seriesDuplicate, { t: nowMs, v: currentDuplicate - lastDuplicate }); + sparkline(document.getElementById('sparkTxPoolTps') as HTMLElement, + seriesTxPool, { t: nowMs, v: currentTxPool - lastTxPool }); + sparkline(document.getElementById('sparkBlockTps') as HTMLElement, + seriesBlock, { t: nowMs, v: currentBlock - lastBlock }); + + // Show TPS values + updateText(blockTpsValue, formatDec((currentBlock - lastBlock) / diff)); + updateText(receivedTpsValue, formatDec((currentReceived - lastReceived) / diff)); + updateText(txPoolTpsValue, formatDec((currentTxPool - lastTxPool) / diff)); + updateText(duplicateTpsValue, formatDec((currentDuplicate - lastDuplicate) / diff)); + updateText(hashesReceivedTpsValue, formatDec((currentHashesReceived - lastHashesReceived) / diff)); + } + + // Update "last" values for next iteration + lastNow = currentNow; + lastReceived = currentReceived; + lastTxPool = currentTxPool; + lastBlock = currentBlock; + lastDuplicate = currentDuplicate; + lastHashesReceived = currentHashesReceived; +} + +const sse = new EventSource("/data/events"); +sse.addEventListener("nodeData", (e) => { + const data = JSON.parse(e.data) as NodeData; + + var newTitle = `Nethermind [${data.network}]${(data.instance ? ' - ' + data.instance : '')}`; + if (document.title != newTitle) { + document.title = newTitle; + } + updateText(version, data.version); + updateText(network, data.network); + // Update uptime text + updateText(upTime, formatDuration(data.uptime)); +}); +sse.addEventListener("txNodes", (e) => { + const data = JSON.parse(e.data) as INode[]; + txPoolNodes = data; +}); +sse.addEventListener("txLinks", (e) => { + if (document.hidden) return; + const data = JSON.parse(e.data) as TxPool; + updateTxPool(data); +}); +let lastGasLimit = 30_000_000; +sse.addEventListener("processed", (e) => { + if (document.hidden) return; + const data = JSON.parse(e.data) as Processed; + + updateText(minGas, data.minGas.toFixed(2)); + updateText(medianGas, data.medianGas.toFixed(2)); + updateText(aveGas, data.aveGas.toFixed(2)); + updateText(maxGas, data.maxGas.toFixed(2)); + updateText(gasLimit, format(data.gasLimit)); + updateText(gasLimitDelta, data.gasLimit > lastGasLimit ? '👆' : data.gasLimit < lastGasLimit ? '👇' : '👈'); + + lastGasLimit = data.gasLimit; +}); +sse.addEventListener("forkChoice", (e) => { + + if (document.hidden) return; + + const data = JSON.parse(e.data) as ForkChoice; + updateText(headBlock, data.head.toFixed(0)); + updateText(safeBlock, data.safe.toFixed(0)); + updateText(finalizedBlock, data.finalized.toFixed(0)); + + updateText(safeBlockDelta, `(${(data.safe - data.head).toFixed(0)})`); + updateText(finalizedBlockDelta, `(${(data.finalized - data.head).toFixed(0)})`); +}); +let maxCpuPercent = 0; +let maxMemoryMb = 0; +sse.addEventListener("system", (e) => { + const data = JSON.parse(e.data) as System; + let memoryMb = data.workingSet / (1024 * 1024); + if (memoryMb > maxMemoryMb) { + maxMemoryMb = memoryMb; + } + let cpuPercent = (data.userPercent + data.privilegedPercent) * 100; + if (cpuPercent > maxCpuPercent) { + maxCpuPercent = cpuPercent; + } + + if (document.hidden) return; + + updateText(upTime, formatDuration(data.uptime)); + + updateText(cpuTime, formatDec(cpuPercent)); + updateText(maxCpuTime, formatDec(maxCpuPercent)); + sparkline(sparkCpu, seriesTotalCpu, { t: performance.now(), v: data.userPercent + data.privilegedPercent }, 300, 100, 60); + + updateText(memory, format(memoryMb)); + updateText(maxMemory, format(maxMemoryMb)); + sparkline(sparkMemory, seriesMemory, { t: performance.now(), v: memoryMb }, 300, 100, 60); +}); + +let logs: string[] = []; +sse.addEventListener("log", (e) => { + const data = JSON.parse(e.data) as string[]; + for (let entry of data) { + const html = ansiConvert.toHtml(entry); + if (logs.length > 100) { logs.shift(); } + logs.push(html); + } +}); + +function appendLogs() { + requestAnimationFrame(appendLogs); + if (logs.length > 0) { + let scroll = false; + if (nodeLog.scrollHeight < 500 || nodeLog.scrollTop < nodeLog.scrollHeight - 500) { + scroll = true; + } + const frag = document.createDocumentFragment(); + for (let i = 0; i < logs.length; i++) { + const newEntry = document.createElement('div'); + newEntry.innerHTML = logs[i]; + frag.appendChild(newEntry); + } + logs = []; + nodeLog.appendChild(frag); + if (scroll) { + window.setTimeout(scrollLogs, 17); + } + } +} + +requestAnimationFrame(appendLogs); +function scrollLogs() { + nodeLog.scrollTop = nodeLog.scrollHeight; +} diff --git a/src/Nethermind/Nethermind.Runner/scripts/format.ts b/src/Nethermind/Nethermind.Runner/scripts/format.ts new file mode 100644 index 00000000000..2ca2911e2fb --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/scripts/format.ts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +/** + * Formats a duration (in milliseconds) as d h m s, or h m s, etc. + */ +export function formatDuration(ms: number): string { + function pad(num: number): string { + return num.toString().padStart(2, '0'); + } + + let totalSeconds = Math.floor(ms / 1000); + let totalMinutes = Math.floor(totalSeconds / 60); + let totalHours = Math.floor(totalMinutes / 60); + + let days = Math.floor(totalHours / 24); + let hours = totalHours % 24; + let minutes = totalMinutes % 60; + let seconds = totalSeconds % 60; + + if (days === 0 && hours === 0 && minutes === 0 && seconds === 0) { + return '0s'; + } + + if (days > 0) { + return `${days}d ${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`; + } + + if (hours > 0) { + return `${hours}h ${pad(minutes)}m ${pad(seconds)}s`; + } + + if (minutes > 0) { + return `${minutes}m ${pad(seconds)}s`; + } + + return `${seconds}s`; +} diff --git a/src/Nethermind/Nethermind.Runner/scripts/sankeyTypes.ts b/src/Nethermind/Nethermind.Runner/scripts/sankeyTypes.ts new file mode 100644 index 00000000000..6c57fb650c8 --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/scripts/sankeyTypes.ts @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + + +// Our custom node interface extends the generic SankeyNode: +export interface SankeyNode { + name: string; + value: number; + targetLinks: SankeyLink[]; + inclusion?: boolean; + + // After .sankey(...) runs, d3 sets the layout properties: + x0: number; + x1: number; + y0: number; + y1: number; + + // And an index (node.index) that can appear after the layout: + index?: number; +} + +// Our custom link interface extends the generic SankeyLink: +export interface SankeyLink { + value: number; + + // Once .sankey(...) has run, these become full references: + source: SankeyNode; + target: SankeyNode; + + // Also set by Sankey: + width?: number; + index?: number; +} diff --git a/src/Nethermind/Nethermind.Runner/scripts/sparkline.ts b/src/Nethermind/Nethermind.Runner/scripts/sparkline.ts new file mode 100644 index 00000000000..47a08e9137d --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/scripts/sparkline.ts @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +import * as d3 from 'd3'; +export interface Datum { + t: number; // e.g., timestamp in ms or any ascending numeric index + v: number; // the actual numeric value +} +interface Margin { + top: number; + right: number; + bottom: number; + left: number; +} + +/** + * A sparkline that slides left as new data arrives. + * Uses d3.scaleTime() with t in milliseconds. + * + * @param element Container element (like a
) for the sparkline. + * @param data Array of {t, v} in ascending order by t (ms). + * @param newDatum A new data point { t, v } to add. + * @param width Outer width of the sparkline SVG (default 300). + * @param height Outer height of the sparkline SVG (default 60). + * @param maxPoints Maximum points in the rolling window (default 50). + */ +export function sparkline( + element: HTMLElement, + data: Datum[], + newDatum: Datum, + width = 80, + height = 44, + maxPoints = 60 +) { + // + // 1. Push the new datum, filter to the last `maxPoints` seconds + // + data.push(newDatum); + const leftEdge = newDatum.t - maxPoints * 1000; + // Keep only data within the fixed time window + data = data.filter(d => d.t >= leftEdge); + + // + // 2. Define margins and compute inner dimensions + // + const margin: Margin = { top: 2, right: 2, bottom: 2, left: 2 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // + // 3. Create or select the and "line-group" + // + let svg = d3.select(element).select('svg'); + if (svg.empty()) { + svg = d3.select(element) + .append('svg') + .attr('width', width) + .attr('height', height); + + // Group for line path: + svg.append('g') + .attr('class', 'line-group') + .attr('transform', `translate(${margin.left},${margin.top})`) + .append('path') + .attr('class', 'sparkline-path') + .attr('fill', 'none') + .attr('stroke', 'steelblue') + .attr('stroke-width', 1.5); + + // Group for y-axis (one-time creation): + svg.append('g') + .attr('class', 'y-axis') + .attr('transform', `translate(${margin.left},${margin.top})`); + } + + // In case width/height changed + svg.attr('width', width).attr('height', height); + + const lineGroup = svg.select('g.line-group'); + const path = lineGroup.select('path.sparkline-path'); + + // + // 4. Build x-scale with a fixed time window [now - maxPoints*1000, now] + // + // Because each point is ~1 second apart and we keep exactly maxPoints seconds, + // the domain width is constant => we won't see "jumps." + // + const now = newDatum.t; + const x = d3.scaleTime() + .domain([new Date(leftEdge), new Date(now)]) + .range([0, innerWidth]); + + // + // 5. Build a y-scale (either dynamic or fixed). + // If your values vary widely, you may see some vertical re-scaling. + // For zero vertical shifting, replace with a fixed domain, e.g. [0, 100]. + // + const [minY, maxY] = d3.extent(data, d => d.v) as [number, number]; + const y = d3.scaleLinear() + .domain([minY, maxY]) + .range([innerHeight, 0]) + .nice(); + + // + // 6. Line generator for the updated data + // + const lineGenerator = d3.line() + .x(d => x(new Date(d.t))!) + .y(d => y(d.v)); + + // Draw the path in its new shape (final position, no transform). + path.datum(data).attr('d', lineGenerator).attr('transform', null); + + // + // 7. "Slide" effect: + // Because we shift the domain by exactly 1 second each time (maxPoints unchanged), + // the horizontal shift in pixels is always innerWidth / maxPoints. + // + // We'll start the path shifted right by that amount, then transition back to 0. + // + if (data.length > 1) { + const xShift = innerWidth / maxPoints; // e.g. 300px wide / 50 = 6px shift + + // Immediately shift path to the right + path.attr('transform', `translate(${xShift},0)`); + + // Then animate to transform(0,0) + path.transition() + .duration(300) + .attr('transform', 'translate(0,0)'); + } +} diff --git a/src/Nethermind/Nethermind.Runner/scripts/txPoolFlow.ts b/src/Nethermind/Nethermind.Runner/scripts/txPoolFlow.ts new file mode 100644 index 00000000000..65b80e4f94b --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/scripts/txPoolFlow.ts @@ -0,0 +1,241 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +import * as d3 from 'd3'; +import { + sankey as d3Sankey, + sankeyLinkHorizontal, + sankeyCenter, + SankeyLayout +} from 'd3-sankey'; +import { TxPool, ILink, INode } from './types'; +import { SankeyNode, SankeyLink } from './sankeyTypes'; + +export class TxPoolFlow { + private svg: d3.Selection; + private rectG: d3.Selection; + private linkG: d3.Selection; + private nodeG: d3.Selection; + + private sankeyGenerator: SankeyLayout; + private width = 1280; + private height = 350; + private defs: d3.Selection; + + private blueColors = [ + '#E1F5FE', '#B3E5FC', '#81D4FA', '#4FC3F7', + '#29B6F6', '#03A9F4', '#039BE5', '#0288D1', + '#0277BD', '#01579B' + ]; + + private orangeColors = [ + '#FFF5e1', '#FFE0B2', '#FFCC80', '#FFB74D', + '#FFA726', '#FF9800', '#FB8C00', '#F57C00', + '#EF6C00', '#E65100' + ]; + + constructor(container: string) { + this.svg = d3.select(container) + .append('svg') + .attr('width', this.width) + .attr('height', this.height) + .attr('viewBox', [0, 0, this.width, this.height]) + .style('max-width', '100%') + .style('height', 'auto'); + this.defs = this.svg.append('defs'); + // Prepare gradients + let colors = this.blueColors.slice(5, -1); + colors = [...colors, ...colors, ...colors, ...colors]; + + this.initGradient('blue-flow', colors); + + // High-level groups + this.rectG = this.svg.append('g').attr('stroke', '#000'); + this.linkG = this.svg.append('g').attr('fill', 'none').style('mix-blend-mode', 'normal'); + this.nodeG = this.svg.append('g'); + + // Sankey layout + this.sankeyGenerator = d3Sankey() + .nodeId((n) => n.name) + .nodeAlign(sankeyCenter) + .nodeWidth(10) + .nodePadding(30) + .nodeSort((a, b) => { + if (a.inclusion && b.inclusion) { + return a.name < b.name ? -1 : 1; + } + if (a.inclusion) return -1; + if (b.inclusion) return 1; + return a.name < b.name ? 1 : -1; + }) + .linkSort((a, b) => { + if (a.target.inclusion && b.target.inclusion) { + return a.source.name < b.source.name ? -1 : 1; + } + if (a.target.inclusion) return -1; + if (b.target.inclusion) return 1; + return a.target.name < b.target.name ? 1 : -1; + }) + .extent([[100, 20], [this.width - 100, this.height - 25]]); + } + + private initGradient(name: string, colors: string[]): void { + const flow = this.defs.append('linearGradient') + .attr('id', name) + .attr('x1', '0%') + .attr('y1', '0%') + .attr('x2', '100%') + .attr('y2', '0') + .attr('spreadMethod', 'reflect') + .attr('gradientUnits', 'userSpaceOnUse'); + + flow.selectAll('stop') + .data(colors) + .enter() + .append('stop') + .attr('offset', (_, i) => i / (colors.length - 1)) + .attr('stop-color', (d) => d); + + flow.append('animate') + .attr('attributeName', 'x1') + .attr('values', '0%;200%') + .attr('dur', '12s') + .attr('repeatCount', 'indefinite'); + + flow.append('animate') + .attr('attributeName', 'x2') + .attr('values', '100%;300%') + .attr('dur', '12s') + .attr('repeatCount', 'indefinite'); + } + + private isRightAligned(d: SankeyNode): boolean { + return !d.inclusion; + } + + /** + * Update the Sankey diagram. + */ + public update(txPoolNodes: INode[], data: TxPool): void { + // Filter out zero-value links + const filteredLinks: ILink[] = []; + const usedNodes: Record = {}; + + for (const link of data.links) { + if (link.value > 0) { + filteredLinks.push(link); + usedNodes[link.source] = true; + usedNodes[link.target] = true; + } + } + + const filteredNodes = txPoolNodes.filter((n) => usedNodes[n.name]); + + // Build sankey input + const sankeyData = { + nodes: filteredNodes.map((n) => ({ ...n })), + links: filteredLinks.map((l) => ({ ...l })) + }; + + // D3 sankey modifies sankeyData in-place, but also returns typed arrays + const { nodes, links } = this.sankeyGenerator(sankeyData) as { nodes: SankeyNode[], links: SankeyLink[]}; + + // ====== Rectangles for nodes ====== + this.rectG + .selectAll('rect') + .data(nodes, (d) => d.name) + .join('rect') + .attr('x', (d) => d.x0) + .attr('y', (d) => d.y0) + .attr('height', (d) => d.y1 - d.y0) + .attr('width', (d) => d.x1 - d.x0) + .attr('fill', (d) => { + if (d.name === 'P2P Network') { + d.value = data.hashesReceived; + } + if (d.inclusion) { + if (d.name === 'Tx Pool' || d.name === 'Added To Block') { + return '#FFA726'; + } + return '#00BFF2'; + } + return '#555'; + }); + + // ====== Paths for links ====== + this.linkG + .selectAll('path') + .data(links, (d) => d.index!) // d.index assigned by sankey + .join('path') + .attr('d', sankeyLinkHorizontal()) + .attr('stroke', (d) => (d.target.inclusion ? 'url(#blue-flow)' : '#333')) + .attr('stroke-width', (d) => Math.max(1, d.width ?? 1)); + + // ====== Labels on nodes ====== + // Using the .join(...) pattern + const textSel = this.nodeG + .selectAll('text') + .data(nodes, (d) => d.name) + .join( + // ENTER + (enter) => enter + .append('text') + .attr('data-last', '0'), // initialize + // UPDATE + (update) => update, + // EXIT + (exit) => exit.remove() + ); + + textSel + .attr('data-last', function (d) { + // If there's an old data-current, preserve it; else '0' + const oldCurrent = d3.select(this).attr('data-current'); + return oldCurrent || '0'; + }) + .attr('data-current', (d) => { + // Summation of target links if you prefer, or just d.value + const targetSum = (d.targetLinks || []).reduce((acc, l) => acc + (l.value || 0), 0); + // Example: whichever is nonzero + return targetSum || d.value || 0; + }) + .attr('x', (d) => (this.isRightAligned(d) ? d.x1 + 6 : d.x0 - 6)) + .attr('y', (d) => (d.y0 + d.y1) / 2) + .attr('dy', '-0.5em') + .attr('text-anchor', (d) => (this.isRightAligned(d) ? 'start' : 'end')) + .text((d) => d.name) + // Now add a for the numeric value + .each(function () { + // 'this' is the element + d3.select(this) + .selectAll('tspan.number') + .data([0]) // ensure exactly one + .join('tspan') + .attr('class', 'number') + .attr('x', () => { + const nodeData = d3.select(this).datum() as SankeyNode; + return nodeData && nodeData.inclusion + ? nodeData.x1 + 6 + : nodeData.x0 - 6; + }) + .attr('dy', '1em'); + }); + + // Transition & tween for the numeric part + textSel.selectAll('tspan.number') + .transition() + .duration(500) + .tween('text', function () { + // The parent has the data-last / data-current + const tspan = d3.select(this); + const parentText = d3.select(this.parentNode as SVGTextElement); + const currentValue = parentText.empty() ? 0 : parseFloat(parentText.attr('data-last') || '0'); + const targetValue = parentText.empty() ? 0 : parseFloat(parentText.attr('data-current') || '0'); + + const interp = d3.interpolateNumber(currentValue, targetValue); + return function (t) { + tspan.text(d3.format(',.0f')(interp(t))); + }; + }); + } +} diff --git a/src/Nethermind/Nethermind.Runner/scripts/types.ts b/src/Nethermind/Nethermind.Runner/scripts/types.ts new file mode 100644 index 00000000000..8b3d977673e --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/scripts/types.ts @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +export interface INode { + name: string; + inclusion?: boolean; +} +export interface ILink { + source: string; + target: string; + value: number; +} +export interface TxPool { + pooledTx: number; + pooledBlobTx: number; + hashesReceived: number; + links: ILink[]; +} + +export interface NodeData { + uptime: number; + instance: string; + network: string; + syncType: string; + pruningMode: string; + version: string; + commit: string; + runtime: string; +} + +export interface Processed +{ + blockCount: number; + blockFrom: number; + blockTo: number; + processingMs: number; + slotMs: number; + mgasPerSecond: number; + minGas: number; + medianGas: number; + aveGas: number; + maxGas: number; + gasLimit: number; +} + +export interface ForkChoice { + head: number; + safe: number; + finalized: number; +} + +export interface System { + uptime: number, + userPercent: number; + privilegedPercent: number; + workingSet: number; +} diff --git a/src/Nethermind/Nethermind.Runner/tsconfig.json b/src/Nethermind/Nethermind.Runner/tsconfig.json new file mode 100644 index 00000000000..a1f494d8155 --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "noImplicitAny": false, + "noEmitOnError": true, + "removeComments": false, + "sourceMap": true, + "module": "System", + "moduleResolution": "node", + "target": "ES2024", + "lib": [ + "ES2024", + "dom" + ], + "outFile": "wwwroot/js" + }, + "include": [ + "scripts/**/*" + ], + "exclude": [ + "node_modules", + "wwwroot" + ] +} diff --git a/src/Nethermind/Nethermind.Runner/wwwroot/css/app.css b/src/Nethermind/Nethermind.Runner/wwwroot/css/app.css new file mode 100644 index 00000000000..e3c6a5883b7 --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/wwwroot/css/app.css @@ -0,0 +1,236 @@ +/* latin-ext */ +@font-face { + font-family: 'DM Sans'; + font-style: normal; + font-weight: 100 1000; + font-display: swap; + src: url(../fonts/dm-sans-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'DM Sans'; + font-style: normal; + font-weight: 100 1000; + font-display: swap; + src: url(../fonts/dm-sans-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* latin-ext */ +@font-face { + font-family: 'DM Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/dm-mono-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'DM Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/dm-mono-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* latin-ext */ +@font-face { + font-family: 'Exo'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/exo-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Exo'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/exo-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + + + + + +html, body { + margin: 0; + padding: 10px; + overflow: hidden; + background: black; + color: white; + font-family: "DM Sans", serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 14px; +} + +svg { + font-family: "DM Sans", serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; +} +.nowrap { + white-space: nowrap; +} +.number { + font-family: "DM Mono", serif; + font-weight: 400; + font-style: normal; + font-size: 12px; +} + +#nodeLog { + font-family: "DM Mono", serif; + font-size: 12px; + white-space: pre; + overflow: hidden auto; + height: 450px; +} + +#nodeLog > div { + min-height: 16px +} + + text { + fill: white; + text-shadow: 1px 1px 1px black; + } + +.link { + fill: none; + stroke: #000; + stroke-opacity: .2; +} + + .link:hover { + stroke-opacity: .5; + } + + +#txPool { + position: absolute; + bottom: 0; + left: 0; + width: 200px; + border-right: 1px solid #555; + border-top: 1px solid #555; + border-top-right-radius: 8px; + padding: 4px 8px 4px 8px; +} + +#txTps { + position: absolute; + bottom: 0; + right: 0; + border-left: 1px solid #555; + border-top: 1px solid #555; + border-top-left-radius: 8px; + padding: 4px 8px 4px 6px; + display: flex; + flex-wrap: nowrap; +} + + + + #txTps > div { + width: 80px; + text-align: center; + border: 1px solid #333; + margin: 4px; + border-radius: 4px; + } + +table { + width: 100%; + border-collapse: collapse; +} + +th { + text-align: left; +} + +.right { + text-align: right; +} + +.logo { + height: 32px; + width: 241px; +} + +.container { + position: relative; + border: 1px solid #555; + padding: 0 0 0 8px; + margin-top: 8px; +} +.title { + font-family: "Exo", serif; + font-optical-sizing: auto; + font-weight: 600; + font-style: normal; + border-right: 1px solid #555; + border-bottom: 1px solid #555; + border-bottom-right-radius: 8px; + padding: 4px 8px 4px 6px; + position: absolute; + top: 0; + left: 0; + background: black; + box-shadow: 4px 4px 8px 4px rgba(0, 0, 0, 0.75); +} + +#header { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; +} + +.max { + position: absolute; + top: 0; + right: 0; +} +.large { + font-size: 28px; + padding-top: 25px; + white-space: nowrap; + position: absolute; + left:8px; + top: 18px; +} +.large > .number { + font-size: 28px; +} +.product { + width: 240px; +} +.version { + text-align: right; + margin-top: -10px; +} + .number.small { + font-size: 10px; + } + +.chainState td { + text-align: center +} +.chainState th { + width: 120px; + text-align: center +} +.chainGas th, .chainGas td { + width: 58px; + text-align: right; +} diff --git a/src/Nethermind/Nethermind.Runner/wwwroot/favicon.webp b/src/Nethermind/Nethermind.Runner/wwwroot/favicon.webp new file mode 100644 index 0000000000000000000000000000000000000000..bce59662e44b76765c113d4bfe6382b23285e442 GIT binary patch literal 9980 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 literal 0 HcmV?d00001 diff --git a/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-sans-latin.woff2 b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-sans-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..5a77bb3dd5e45fbb241bac4b66e00d9fae98cc21 GIT binary patch literal 62792 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 YHZ~h5(IOMlX$tU8AR0JReggOVA0t^Qm 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 mb3oJ=`-@u$0001t`Id(O literal 0 HcmV?d00001 diff --git a/src/Nethermind/Nethermind.Runner/wwwroot/index.html b/src/Nethermind/Nethermind.Runner/wwwroot/index.html new file mode 100644 index 00000000000..e74a63598a1 --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/wwwroot/index.html @@ -0,0 +1,146 @@ + + + + + Nethermind + + + + +

+
+
+
Cpu
+
+
+ % +
+
Max: %
+
+
+
Memory
+
+
+ MB +
+
Max: MB
+
+
+
+
Transaction Flows
+
+ + + + + + + + + + + + + + + + + + + +
Local Tx MemPoolCount
Execution0
Blob0
Total0
+
+
+
+
p2p hashes
+
+
0 tps
+
+
+
tx received
+
+
0 tps
+
+
+
duplicate tx
+
+
0 tps
+
+
+
txpool adds
+
+
0 tps
+
+
+
block adds
+
+
0 tps
+
+
+
+
+
Execution Logs
+
+
+ + + diff --git a/src/Nethermind/Nethermind.Runner/wwwroot/nethermind.svg b/src/Nethermind/Nethermind.Runner/wwwroot/nethermind.svg new file mode 100644 index 00000000000..9fc4f4aca9e --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/wwwroot/nethermind.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Nethermind/Nethermind.Runner/yarn.lock b/src/Nethermind/Nethermind.Runner/yarn.lock new file mode 100644 index 00000000000..175aeb019f6 --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/yarn.lock @@ -0,0 +1,711 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@esbuild/aix-ppc64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461" + integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA== + +"@esbuild/android-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894" + integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg== + +"@esbuild/android-arm@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3" + integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q== + +"@esbuild/android-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb" + integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw== + +"@esbuild/darwin-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936" + integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA== + +"@esbuild/darwin-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9" + integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA== + +"@esbuild/freebsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00" + integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg== + +"@esbuild/freebsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f" + integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q== + +"@esbuild/linux-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43" + integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg== + +"@esbuild/linux-arm@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736" + integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA== + +"@esbuild/linux-ia32@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5" + integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw== + +"@esbuild/linux-loong64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc" + integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ== + +"@esbuild/linux-mips64el@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb" + integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw== + +"@esbuild/linux-ppc64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412" + integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw== + +"@esbuild/linux-riscv64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694" + integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q== + +"@esbuild/linux-s390x@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577" + integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw== + +"@esbuild/linux-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f" + integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q== + +"@esbuild/netbsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6" + integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw== + +"@esbuild/netbsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40" + integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw== + +"@esbuild/openbsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f" + integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A== + +"@esbuild/openbsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205" + integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA== + +"@esbuild/sunos-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6" + integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig== + +"@esbuild/win32-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85" + integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ== + +"@esbuild/win32-ia32@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2" + integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA== + +"@esbuild/win32-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b" + integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== + +"@types/d3-array@*": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + +"@types/d3-axis@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795" + integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c" + integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d" + integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-contour@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231" + integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-delaunay@*": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" + integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== + +"@types/d3-dispatch@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz#096efdf55eb97480e3f5621ff9a8da552f0961e7" + integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ== + +"@types/d3-drag@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" + integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" + integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== + +"@types/d3-ease@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-fetch@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980" + integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a" + integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw== + +"@types/d3-format@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" + integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== + +"@types/d3-geo@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440" + integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b" + integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== + +"@types/d3-interpolate@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" + integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== + +"@types/d3-polygon@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c" + integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== + +"@types/d3-quadtree@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f" + integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== + +"@types/d3-random@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb" + integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== + +"@types/d3-scale-chromatic@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39" + integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== + +"@types/d3-scale@*": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" + integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-selection@*": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3" + integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== + +"@types/d3-shape@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" + integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" + integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== + +"@types/d3-time@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" + integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== + +"@types/d3-timer@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + +"@types/d3-transition@*": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706" + integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" + integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@7.4.3": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2" + integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + +"@types/geojson@*": + version "7946.0.16" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== + +ansi-to-html@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.7.2.tgz#a92c149e4184b571eb29a0135ca001a8e2d710cb" + integrity sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g== + dependencies: + entities "^2.2.0" + +commander@7: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +"d3-array@1 - 2": + version "2.12.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + +"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +d3-axis@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322" + integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== + +d3-brush@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c" + integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "3" + d3-transition "3" + +d3-chord@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966" + integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== + dependencies: + d3-path "1 - 3" + +"d3-color@1 - 3", d3-color@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-contour@4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc" + integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA== + dependencies: + d3-array "^3.2.0" + +d3-delaunay@6: + version "6.0.4" + resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b" + integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== + dependencies: + delaunator "5" + +"d3-dispatch@1 - 3", d3-dispatch@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + +"d3-drag@2 - 3", d3-drag@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + +"d3-dsv@1 - 3", d3-dsv@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" + integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== + dependencies: + commander "7" + iconv-lite "0.6" + rw "1" + +"d3-ease@1 - 3", d3-ease@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +d3-fetch@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22" + integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== + dependencies: + d3-dsv "1 - 3" + +d3-force@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" + integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== + dependencies: + d3-dispatch "1 - 3" + d3-quadtree "1 - 3" + d3-timer "1 - 3" + +"d3-format@1 - 3", d3-format@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +d3-geo@3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.1.tgz#6027cf51246f9b2ebd64f99e01dc7c3364033a4d" + integrity sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q== + dependencies: + d3-array "2.5.0 - 3" + +d3-hierarchy@3: + version "3.1.2" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" + integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== + +"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + +"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-polygon@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" + integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== + +"d3-quadtree@1 - 3", d3-quadtree@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" + integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== + +d3-random@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" + integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== + +d3-sankey@0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/d3-sankey/-/d3-sankey-0.12.3.tgz#b3c268627bd72e5d80336e8de6acbfec9d15d01d" + integrity sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ== + dependencies: + d3-array "1 - 2" + d3-shape "^1.2.0" + +d3-scale-chromatic@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#34c39da298b23c20e02f1a4b239bd0f22e7f1314" + integrity sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ== + dependencies: + d3-color "1 - 3" + d3-interpolate "1 - 3" + +d3-scale@4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +"d3-selection@2 - 3", d3-selection@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + +d3-shape@3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +d3-shape@^1.2.0: + version "1.3.7" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + +"d3-time-format@2 - 4", d3-time-format@4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +"d3-timer@1 - 3", d3-timer@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +"d3-transition@2 - 3", d3-transition@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + +d3@7.9.0: + version "7.9.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-7.9.0.tgz#579e7acb3d749caf8860bd1741ae8d371070cd5d" + integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA== + dependencies: + d3-array "3" + d3-axis "3" + d3-brush "3" + d3-chord "3" + d3-color "3" + d3-contour "4" + d3-delaunay "6" + d3-dispatch "3" + d3-drag "3" + d3-dsv "3" + d3-ease "3" + d3-fetch "3" + d3-force "3" + d3-format "3" + d3-geo "3" + d3-hierarchy "3" + d3-interpolate "3" + d3-path "3" + d3-polygon "3" + d3-quadtree "3" + d3-random "3" + d3-scale "4" + d3-scale-chromatic "3" + d3-selection "3" + d3-shape "3" + d3-time "3" + d3-time-format "4" + d3-timer "3" + d3-transition "3" + d3-zoom "3" + +delaunator@5: + version "5.0.1" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" + integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== + dependencies: + robust-predicates "^3.0.2" + +entities@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +esbuild@0.24.2: + version "0.24.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d" + integrity sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.24.2" + "@esbuild/android-arm" "0.24.2" + "@esbuild/android-arm64" "0.24.2" + "@esbuild/android-x64" "0.24.2" + "@esbuild/darwin-arm64" "0.24.2" + "@esbuild/darwin-x64" "0.24.2" + "@esbuild/freebsd-arm64" "0.24.2" + "@esbuild/freebsd-x64" "0.24.2" + "@esbuild/linux-arm" "0.24.2" + "@esbuild/linux-arm64" "0.24.2" + "@esbuild/linux-ia32" "0.24.2" + "@esbuild/linux-loong64" "0.24.2" + "@esbuild/linux-mips64el" "0.24.2" + "@esbuild/linux-ppc64" "0.24.2" + "@esbuild/linux-riscv64" "0.24.2" + "@esbuild/linux-s390x" "0.24.2" + "@esbuild/linux-x64" "0.24.2" + "@esbuild/netbsd-arm64" "0.24.2" + "@esbuild/netbsd-x64" "0.24.2" + "@esbuild/openbsd-arm64" "0.24.2" + "@esbuild/openbsd-x64" "0.24.2" + "@esbuild/sunos-x64" "0.24.2" + "@esbuild/win32-arm64" "0.24.2" + "@esbuild/win32-ia32" "0.24.2" + "@esbuild/win32-x64" "0.24.2" + +iconv-lite@0.6: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + +robust-predicates@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" + integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== + +rw@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +typescript@5.7.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" + integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== + +yarn@1.22.22: + version "1.22.22" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.22.tgz#ac34549e6aa8e7ead463a7407e1c7390f61a6610" + integrity sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg== diff --git a/src/Nethermind/Nethermind.TxPool/Filters/NullHashTxFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/NullHashTxFilter.cs index 2d79df95d6f..70682eb66a6 100644 --- a/src/Nethermind/Nethermind.TxPool/Filters/NullHashTxFilter.cs +++ b/src/Nethermind/Nethermind.TxPool/Filters/NullHashTxFilter.cs @@ -16,6 +16,7 @@ public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandl { if (tx.Hash is null) { + Metrics.PendingTransactionsNullHash++; return AcceptTxResult.Invalid; } diff --git a/src/Nethermind/Nethermind.TxPool/Filters/SizeTxFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/SizeTxFilter.cs index 9060df8518e..40e6a6dd173 100644 --- a/src/Nethermind/Nethermind.TxPool/Filters/SizeTxFilter.cs +++ b/src/Nethermind/Nethermind.TxPool/Filters/SizeTxFilter.cs @@ -20,6 +20,7 @@ public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandl if (tx.GetLength(shouldCountBlobs: false) > maxSize) { + Metrics.PendingTransactionsSizeTooLarge++; if (logger.IsTrace) logger.Trace($"Skipped adding transaction {tx.ToString(" ")}, max tx size exceeded."); return AcceptTxResult.MaxTxSizeExceeded; } diff --git a/src/Nethermind/Nethermind.TxPool/Metrics.cs b/src/Nethermind/Nethermind.TxPool/Metrics.cs index d825ad8060b..fdb00517ef1 100644 --- a/src/Nethermind/Nethermind.TxPool/Metrics.cs +++ b/src/Nethermind/Nethermind.TxPool/Metrics.cs @@ -149,5 +149,11 @@ public static class Metrics [GaugeMetric] [Description("Number of blob transactions in pool.")] public static long BlobTransactionCount { get; set; } + + public static long PendingTransactionsSizeTooLarge { get; set; } + public static long PendingTransactionsNullHash { get; set; } + public static long TransactionsSourcedPrivateOrderflow { get; internal set; } + public static long TransactionsSourcedMemPool { get; internal set; } + public static long TransactionsReorged { get; internal set; } } } diff --git a/src/Nethermind/Nethermind.TxPool/TxPool.cs b/src/Nethermind/Nethermind.TxPool/TxPool.cs index a9c6f3b90a3..8d4df70c996 100644 --- a/src/Nethermind/Nethermind.TxPool/TxPool.cs +++ b/src/Nethermind/Nethermind.TxPool/TxPool.cs @@ -271,6 +271,8 @@ private void ReAddReorganisedTransactions(Block? previousBlock) { if (previousBlock is not null) { + + Metrics.TransactionsReorged += previousBlock.Transactions.Length; bool isEip155Enabled = _specProvider.GetSpec(previousBlock.Header).IsEip155Enabled; Transaction[] txs = previousBlock.Transactions; for (int i = 0; i < txs.Length; i++) @@ -308,6 +310,7 @@ private void RemoveProcessedTransactions(Block block) using ArrayPoolList blobTxsToSave = new((int)_specProvider.GetSpec(block.Header).MaxBlobCount); long discoveredForPendingTxs = 0; long discoveredForHashCache = 0; + long notInMempoool = 0; long eip1559Txs = 0; long eip7702Txs = 0; long blobTxs = 0; @@ -344,14 +347,23 @@ private void RemoveProcessedTransactions(Block block) eip7702Txs++; } + bool isKnown = true; if (!IsKnown(txHash)) { discoveredForHashCache++; + isKnown = false; } + bool isPending = true; if (!RemoveIncludedTransaction(blockTx)) { discoveredForPendingTxs++; + isPending = false; + } + + if (!isKnown && !isPending) + { + notInMempoool++; } } @@ -369,6 +381,9 @@ private void RemoveProcessedTransactions(Block block) Metrics.Eip7702TransactionsInBlock = eip7702Txs; Metrics.BlobTransactionsInBlock = blobTxs; Metrics.BlobsInBlock = blobs; + + Metrics.TransactionsSourcedPrivateOrderflow += notInMempoool; + Metrics.TransactionsSourcedMemPool += transactionsInBlock - notInMempoool; } } @@ -889,21 +904,23 @@ private static void WriteTxPoolReport(in ILogger logger) ------------------------------------------------ Discarded at Filter Stage: 1. NotSupportedTxType {Metrics.PendingTransactionsNotSupportedTxType,24:N0} -2. GasLimitTooHigh: {Metrics.PendingTransactionsGasLimitTooHigh,24:N0} -3. TooLow PriorityFee: {Metrics.PendingTransactionsTooLowPriorityFee,24:N0} -4. Too Low Fee: {Metrics.PendingTransactionsTooLowFee,24:N0} -5. Malformed {Metrics.PendingTransactionsMalformed,24:N0} -6. Duplicate: {Metrics.PendingTransactionsKnown,24:N0} -7. Unknown Sender: {Metrics.PendingTransactionsUnresolvableSender,24:N0} -8. Conflicting TxType {Metrics.PendingTransactionsConflictingTxType,24:N0} -9. NonceTooFarInFuture {Metrics.PendingTransactionsNonceTooFarInFuture,24:N0} -10. Zero Balance: {Metrics.PendingTransactionsZeroBalance,24:N0} -11. Balance < tx.value: {Metrics.PendingTransactionsBalanceBelowValue,24:N0} -12. Balance Too Low: {Metrics.PendingTransactionsTooLowBalance,24:N0} -13. Nonce used: {Metrics.PendingTransactionsLowNonce,24:N0} -14. Nonces skipped: {Metrics.PendingTransactionsNonceGap,24:N0} -15. Failed replacement {Metrics.PendingTransactionsPassedFiltersButCannotReplace,24:N0} -16. Cannot Compete: {Metrics.PendingTransactionsPassedFiltersButCannotCompeteOnFees,24:N0} +2. Tx Too Large: {Metrics.PendingTransactionsSizeTooLarge,24:N0} +3. GasLimitTooHigh: {Metrics.PendingTransactionsGasLimitTooHigh,24:N0} +4. TooLow PriorityFee: {Metrics.PendingTransactionsTooLowPriorityFee,24:N0} +5. Too Low Fee: {Metrics.PendingTransactionsTooLowFee,24:N0} +6. Malformed: {Metrics.PendingTransactionsMalformed,24:N0} +7. Null Hash: {Metrics.PendingTransactionsNullHash,24:N0} +8. Duplicate: {Metrics.PendingTransactionsKnown,24:N0} +9. Unknown Sender: {Metrics.PendingTransactionsUnresolvableSender,24:N0} +10. Conflicting TxType: {Metrics.PendingTransactionsConflictingTxType,24:N0} +11. NonceTooFarInFuture {Metrics.PendingTransactionsNonceTooFarInFuture,24:N0} +12. Zero Balance: {Metrics.PendingTransactionsZeroBalance,24:N0} +13. Balance < tx.value: {Metrics.PendingTransactionsBalanceBelowValue,24:N0} +14. Balance Too Low: {Metrics.PendingTransactionsTooLowBalance,24:N0} +15. Nonce used: {Metrics.PendingTransactionsLowNonce,24:N0} +16. Nonces skipped: {Metrics.PendingTransactionsNonceGap,24:N0} +17. Failed replacement {Metrics.PendingTransactionsPassedFiltersButCannotReplace,24:N0} +18. Cannot Compete: {Metrics.PendingTransactionsPassedFiltersButCannotCompeteOnFees,24:N0} ------------------------------------------------ Validated via State: {Metrics.PendingTransactionsWithExpensiveFiltering,24:N0} ------------------------------------------------ diff --git a/src/Nethermind/Nethermind.sln b/src/Nethermind/Nethermind.sln index 7496bb3c903..cfa737f8fac 100644 --- a/src/Nethermind/Nethermind.sln +++ b/src/Nethermind/Nethermind.sln @@ -208,8 +208,10 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nethermind.Era1", "Nethermind.Era1\Nethermind.Era1.csproj", "{AFD974A9-C907-4A9F-859D-5698A67B58A8}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nethermind.Era1.Test", "Nethermind.Era.Test\Nethermind.Era1.Test.csproj", "{08B2B720-5DD4-4F69-8DD1-DF62E2B405FC}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{06E9C864-638E-4DB4-B40B-F73475EA0449}" ProjectSection(SolutionItems) = preProject + ..\..\.gitignore = ..\..\.gitignore Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props nuget.config = nuget.config @@ -599,14 +601,6 @@ Global {AD151E35-4BBC-4A83-8F57-CC8665F6E007}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD151E35-4BBC-4A83-8F57-CC8665F6E007}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD151E35-4BBC-4A83-8F57-CC8665F6E007}.Release|Any CPU.Build.0 = Release|Any CPU - {AFD974A9-C907-4A9F-859D-5698A67B58A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AFD974A9-C907-4A9F-859D-5698A67B58A8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AFD974A9-C907-4A9F-859D-5698A67B58A8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AFD974A9-C907-4A9F-859D-5698A67B58A8}.Release|Any CPU.Build.0 = Release|Any CPU - {08B2B720-5DD4-4F69-8DD1-DF62E2B405FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08B2B720-5DD4-4F69-8DD1-DF62E2B405FC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08B2B720-5DD4-4F69-8DD1-DF62E2B405FC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08B2B720-5DD4-4F69-8DD1-DF62E2B405FC}.Release|Any CPU.Build.0 = Release|Any CPU {32E5D15A-F6A6-40EA-9F31-05FE9251F958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {32E5D15A-F6A6-40EA-9F31-05FE9251F958}.Debug|Any CPU.Build.0 = Debug|Any CPU {32E5D15A-F6A6-40EA-9F31-05FE9251F958}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -615,6 +609,14 @@ Global {AD09FBCB-5496-499B-9129-B6D139A65B6F}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD09FBCB-5496-499B-9129-B6D139A65B6F}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD09FBCB-5496-499B-9129-B6D139A65B6F}.Release|Any CPU.Build.0 = Release|Any CPU + {AFD974A9-C907-4A9F-859D-5698A67B58A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFD974A9-C907-4A9F-859D-5698A67B58A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFD974A9-C907-4A9F-859D-5698A67B58A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFD974A9-C907-4A9F-859D-5698A67B58A8}.Release|Any CPU.Build.0 = Release|Any CPU + {08B2B720-5DD4-4F69-8DD1-DF62E2B405FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08B2B720-5DD4-4F69-8DD1-DF62E2B405FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08B2B720-5DD4-4F69-8DD1-DF62E2B405FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08B2B720-5DD4-4F69-8DD1-DF62E2B405FC}.Release|Any CPU.Build.0 = Release|Any CPU {6528010D-7DCE-4935-9785-5270FF515F3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6528010D-7DCE-4935-9785-5270FF515F3E}.Debug|Any CPU.Build.0 = Debug|Any CPU {6528010D-7DCE-4935-9785-5270FF515F3E}.Release|Any CPU.ActiveCfg = Release|Any CPU From 22e4a566ab3f0a3bdd7500d6699694a847e676e5 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Mon, 27 Jan 2025 16:59:32 +0000 Subject: [PATCH 2/5] Lower resource use --- .../Nethermind.Runner/scripts/app.ts | 91 +++++++++++-------- .../Nethermind.Runner/scripts/sparkline.ts | 5 +- 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/src/Nethermind/Nethermind.Runner/scripts/app.ts b/src/Nethermind/Nethermind.Runner/scripts/app.ts index 0b65a092a8f..97f13589ae8 100644 --- a/src/Nethermind/Nethermind.Runner/scripts/app.ts +++ b/src/Nethermind/Nethermind.Runner/scripts/app.ts @@ -83,17 +83,9 @@ let txPoolNodes: INode[] = null; * Main function to start polling data and updating the UI. */ function updateTxPool(txPool: TxPool) { - - if (!txPoolNodes) { - return; - } - // Update Sankey - txPoolFlow.update(txPoolNodes, txPool); - - // Update numeric indicators - updateText(txPoolValue, d3.format(',.0f')(txPool.pooledTx)); - updateText(blobTxPoolValue, d3.format(',.0f')(txPool.pooledBlobTx)); - updateText(totalValue, d3.format(',.0f')(txPool.pooledTx + txPool.pooledBlobTx)); + const nowMs = performance.now(); + const currentNow = nowMs / 1000; + const diff = currentNow - lastNow; // Summarize link flows to compute TPS let currentReceived = 0; @@ -116,30 +108,48 @@ function updateTxPool(txPool: TxPool) { } } const currentHashesReceived = txPool.hashesReceived; - const nowMs = performance.now(); - const currentNow = nowMs / 1000; if (lastNow !== 0) { - const diff = currentNow - lastNow; - - // Update the sparkline for each type - sparkline(document.getElementById('sparkHashesTps') as HTMLElement, - seriesHashes, { t: nowMs, v: currentHashesReceived - lastHashesReceived }); - sparkline(document.getElementById('sparkReceivedTps') as HTMLElement, - seriesReceived, { t: nowMs, v: currentReceived - lastReceived }); - sparkline(document.getElementById('sparkDuplicateTps') as HTMLElement, - seriesDuplicate, { t: nowMs, v: currentDuplicate - lastDuplicate }); - sparkline(document.getElementById('sparkTxPoolTps') as HTMLElement, - seriesTxPool, { t: nowMs, v: currentTxPool - lastTxPool }); - sparkline(document.getElementById('sparkBlockTps') as HTMLElement, - seriesBlock, { t: nowMs, v: currentBlock - lastBlock }); - - // Show TPS values - updateText(blockTpsValue, formatDec((currentBlock - lastBlock) / diff)); - updateText(receivedTpsValue, formatDec((currentReceived - lastReceived) / diff)); - updateText(txPoolTpsValue, formatDec((currentTxPool - lastTxPool) / diff)); - updateText(duplicateTpsValue, formatDec((currentDuplicate - lastDuplicate) / diff)); - updateText(hashesReceivedTpsValue, formatDec((currentHashesReceived - lastHashesReceived) / diff)); + seriesHashes.push({ t: nowMs, v: currentHashesReceived - lastHashesReceived }); + if (seriesHashes.length > 60) { seriesHashes.shift(); } + seriesReceived.push({ t: nowMs, v: currentReceived - lastReceived }); + if (seriesReceived.length > 60) { seriesReceived.shift(); } + seriesDuplicate.push({ t: nowMs, v: currentDuplicate - lastDuplicate }); + if (seriesDuplicate.length > 60) { seriesDuplicate.shift(); } + seriesTxPool.push({ t: nowMs, v: currentTxPool - lastTxPool }); + if (seriesTxPool.length > 60) { seriesTxPool.shift(); } + seriesBlock.push({ t: nowMs, v: currentBlock - lastBlock }); + if (seriesBlock.length > 60) { seriesBlock.shift(); } + } + + + if (!document.hidden) { + if (!txPoolNodes) { + return; + } + // Update Sankey + txPoolFlow.update(txPoolNodes, txPool); + + // Update numeric indicators + updateText(txPoolValue, d3.format(',.0f')(txPool.pooledTx)); + updateText(blobTxPoolValue, d3.format(',.0f')(txPool.pooledBlobTx)); + updateText(totalValue, d3.format(',.0f')(txPool.pooledTx + txPool.pooledBlobTx)); + + if (lastNow !== 0) { + // Update the sparkline for each type + sparkline(document.getElementById('sparkHashesTps') as HTMLElement, seriesHashes); + sparkline(document.getElementById('sparkReceivedTps') as HTMLElement, seriesReceived); + sparkline(document.getElementById('sparkDuplicateTps') as HTMLElement, seriesDuplicate); + sparkline(document.getElementById('sparkTxPoolTps') as HTMLElement, seriesTxPool); + sparkline(document.getElementById('sparkBlockTps') as HTMLElement, seriesBlock); + + // Show TPS values + updateText(blockTpsValue, formatDec((currentBlock - lastBlock) / diff)); + updateText(receivedTpsValue, formatDec((currentReceived - lastReceived) / diff)); + updateText(txPoolTpsValue, formatDec((currentTxPool - lastTxPool) / diff)); + updateText(duplicateTpsValue, formatDec((currentDuplicate - lastDuplicate) / diff)); + updateText(hashesReceivedTpsValue, formatDec((currentHashesReceived - lastHashesReceived) / diff)); + } } // Update "last" values for next iteration @@ -169,7 +179,6 @@ sse.addEventListener("txNodes", (e) => { txPoolNodes = data; }); sse.addEventListener("txLinks", (e) => { - if (document.hidden) return; const data = JSON.parse(e.data) as TxPool; updateTxPool(data); }); @@ -199,6 +208,7 @@ sse.addEventListener("forkChoice", (e) => { updateText(safeBlockDelta, `(${(data.safe - data.head).toFixed(0)})`); updateText(finalizedBlockDelta, `(${(data.finalized - data.head).toFixed(0)})`); }); + let maxCpuPercent = 0; let maxMemoryMb = 0; sse.addEventListener("system", (e) => { @@ -212,17 +222,26 @@ sse.addEventListener("system", (e) => { maxCpuPercent = cpuPercent; } + const now = performance.now(); + let newCpuDatum = { t: now, v: data.userPercent + data.privilegedPercent }; + seriesTotalCpu.push(newCpuDatum); + if (seriesTotalCpu.length > 60) { seriesTotalCpu.shift(); } + + let newMemoryDatum = { t: now, v: memoryMb }; + seriesMemory.push(newMemoryDatum); + if (seriesMemory.length > 60) { seriesMemory.shift(); } + if (document.hidden) return; updateText(upTime, formatDuration(data.uptime)); updateText(cpuTime, formatDec(cpuPercent)); updateText(maxCpuTime, formatDec(maxCpuPercent)); - sparkline(sparkCpu, seriesTotalCpu, { t: performance.now(), v: data.userPercent + data.privilegedPercent }, 300, 100, 60); + sparkline(sparkCpu, seriesTotalCpu, 300, 100, 60); updateText(memory, format(memoryMb)); updateText(maxMemory, format(maxMemoryMb)); - sparkline(sparkMemory, seriesMemory, { t: performance.now(), v: memoryMb }, 300, 100, 60); + sparkline(sparkMemory, seriesMemory, 300, 100, 60); }); let logs: string[] = []; diff --git a/src/Nethermind/Nethermind.Runner/scripts/sparkline.ts b/src/Nethermind/Nethermind.Runner/scripts/sparkline.ts index 47a08e9137d..8afe5edb94c 100644 --- a/src/Nethermind/Nethermind.Runner/scripts/sparkline.ts +++ b/src/Nethermind/Nethermind.Runner/scripts/sparkline.ts @@ -22,20 +22,19 @@ interface Margin { * @param newDatum A new data point { t, v } to add. * @param width Outer width of the sparkline SVG (default 300). * @param height Outer height of the sparkline SVG (default 60). - * @param maxPoints Maximum points in the rolling window (default 50). + * @param maxPoints Maximum points in the rolling window (default 60). */ export function sparkline( element: HTMLElement, data: Datum[], - newDatum: Datum, width = 80, height = 44, maxPoints = 60 ) { + const newDatum: Datum = data[data.length - 1]; // // 1. Push the new datum, filter to the last `maxPoints` seconds // - data.push(newDatum); const leftEdge = newDatum.t - maxPoints * 1000; // Keep only data within the fixed time window data = data.filter(d => d.t >= leftEdge); From 98fa4ca841277c36867460c429c078fd07493ffc Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Mon, 27 Jan 2025 17:02:07 +0000 Subject: [PATCH 3/5] Less repitition --- .../Nethermind.Runner/scripts/app.ts | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/Nethermind/Nethermind.Runner/scripts/app.ts b/src/Nethermind/Nethermind.Runner/scripts/app.ts index 97f13589ae8..19637fbcd73 100644 --- a/src/Nethermind/Nethermind.Runner/scripts/app.ts +++ b/src/Nethermind/Nethermind.Runner/scripts/app.ts @@ -64,6 +64,13 @@ let lastDuplicate = 0; let lastHashesReceived = 0; let lastNow = 0; +function addCapped(array: Datum[], datum: Datum) { + array.push(datum); + if (array.length > 60) { + array.shift(); + } +} + function updateText(element: HTMLElement, value: string): void { if (element.innerText !== value) { // Don't update the DOM if the value is the same @@ -110,16 +117,11 @@ function updateTxPool(txPool: TxPool) { const currentHashesReceived = txPool.hashesReceived; if (lastNow !== 0) { - seriesHashes.push({ t: nowMs, v: currentHashesReceived - lastHashesReceived }); - if (seriesHashes.length > 60) { seriesHashes.shift(); } - seriesReceived.push({ t: nowMs, v: currentReceived - lastReceived }); - if (seriesReceived.length > 60) { seriesReceived.shift(); } - seriesDuplicate.push({ t: nowMs, v: currentDuplicate - lastDuplicate }); - if (seriesDuplicate.length > 60) { seriesDuplicate.shift(); } - seriesTxPool.push({ t: nowMs, v: currentTxPool - lastTxPool }); - if (seriesTxPool.length > 60) { seriesTxPool.shift(); } - seriesBlock.push({ t: nowMs, v: currentBlock - lastBlock }); - if (seriesBlock.length > 60) { seriesBlock.shift(); } + addCapped(seriesHashes, { t: nowMs, v: currentHashesReceived - lastHashesReceived }); + addCapped(seriesReceived, { t: nowMs, v: currentReceived - lastReceived }); + addCapped(seriesDuplicate, { t: nowMs, v: currentDuplicate - lastDuplicate }); + addCapped(seriesTxPool, { t: nowMs, v: currentTxPool - lastTxPool }); + addCapped(seriesBlock, { t: nowMs, v: currentBlock - lastBlock }); } @@ -223,13 +225,8 @@ sse.addEventListener("system", (e) => { } const now = performance.now(); - let newCpuDatum = { t: now, v: data.userPercent + data.privilegedPercent }; - seriesTotalCpu.push(newCpuDatum); - if (seriesTotalCpu.length > 60) { seriesTotalCpu.shift(); } - - let newMemoryDatum = { t: now, v: memoryMb }; - seriesMemory.push(newMemoryDatum); - if (seriesMemory.length > 60) { seriesMemory.shift(); } + addCapped(seriesTotalCpu, { t: now, v: data.userPercent + data.privilegedPercent }); + addCapped(seriesMemory, { t: now, v: memoryMb }); if (document.hidden) return; From 147205f11c31f11aa6cd5ed3706ab2b18f8293e4 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 13 Feb 2025 04:46:16 +0000 Subject: [PATCH 4/5] Add head block info --- .../Nethermind.Blockchain/BlockTree.cs | 2 +- .../Nethermind.Blockchain/IBlockTree.cs | 4 +- .../Nethermind.Runner/Monitoring/DataFeed.cs | 134 +++++++- .../Nethermind.Runner.csproj | 4 + .../Nethermind.Runner/scripts/app.ts | 63 +++- .../Nethermind.Runner/scripts/treeMap.ts | 322 ++++++++++++++++++ .../Nethermind.Runner/scripts/txPoolFlow.ts | 10 +- .../Nethermind.Runner/scripts/types.ts | 56 ++- .../Nethermind.Runner/scripts/utilities.ts | 270 +++++++++++++++ .../Nethermind.Runner/wwwroot/index.html | 37 +- 10 files changed, 883 insertions(+), 19 deletions(-) create mode 100644 src/Nethermind/Nethermind.Runner/scripts/treeMap.ts create mode 100644 src/Nethermind/Nethermind.Runner/scripts/utilities.ts diff --git a/src/Nethermind/Nethermind.Blockchain/BlockTree.cs b/src/Nethermind/Nethermind.Blockchain/BlockTree.cs index f279fb994bd..3247a1e0fe6 100644 --- a/src/Nethermind/Nethermind.Blockchain/BlockTree.cs +++ b/src/Nethermind/Nethermind.Blockchain/BlockTree.cs @@ -1562,7 +1562,7 @@ public void ForkChoiceUpdated(Hash256? finalizedBlockHash, Hash256? safeBlockHas var finalizedNumber = FindHeader(FinalizedHash, BlockTreeLookupOptions.DoNotCreateLevelIfMissing)?.Number; var safeNumber = FindHeader(safeBlockHash, BlockTreeLookupOptions.DoNotCreateLevelIfMissing)?.Number; - OnForkChoiceUpdated?.Invoke(this, new(Head?.Number ?? 0, safeNumber ?? 0, finalizedNumber ?? 0)); + OnForkChoiceUpdated?.Invoke(this, new(Head, safeNumber ?? 0, finalizedNumber ?? 0)); } } } diff --git a/src/Nethermind/Nethermind.Blockchain/IBlockTree.cs b/src/Nethermind/Nethermind.Blockchain/IBlockTree.cs index 30d2508a5f8..85b03204420 100644 --- a/src/Nethermind/Nethermind.Blockchain/IBlockTree.cs +++ b/src/Nethermind/Nethermind.Blockchain/IBlockTree.cs @@ -180,9 +180,9 @@ AddBlockResult Insert(Block block, BlockTreeInsertBlockOptions insertBlockOption void RecalculateTreeLevels(); - public readonly struct ForkChoice(long head, long safe, long finalized) + public readonly struct ForkChoice(Block? head, long safe, long finalized) { - public readonly long Head => head; + public readonly Block? Head => head; public readonly long Safe => safe; public readonly long Finalized => finalized; } diff --git a/src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs b/src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs index c1f47ada43c..395275ac138 100644 --- a/src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs +++ b/src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs @@ -9,12 +9,20 @@ using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; - using Nethermind.Api; using Nethermind.Blockchain; +using Nethermind.Blockchain.Receipts; using Nethermind.Consensus.Processing; +using Nethermind.Core; +using Nethermind.Evm; +using Nethermind.Core.Crypto; +using Nethermind.Core.Specs; +using Nethermind.JsonRpc.Data; using Nethermind.Runner.Monitoring.TransactionPool; +using Nethermind.Serialization.Json; using Nethermind.TxPool; +using Nethermind.Int256; +using Nethermind.Serialization.Rlp; namespace Nethermind.Runner.Monitoring; @@ -23,12 +31,17 @@ public class DataFeed public static long StartTime { get; set; } private readonly ITxPool _txPool; + private readonly ISpecProvider _specProvider; + private readonly IReceiptFinder _receiptFinder; private readonly IBlockTree _blockTree; + private readonly BlockDecoder _blockDecoder = new(); public DataFeed(INethermindApi api) { _txPool = api.TxPool; _blockTree = api.BlockTree; + _specProvider = api.SpecProvider; + _receiptFinder = api.ReceiptFinder; ArgumentNullException.ThrowIfNull(_txPool); ArgumentNullException.ThrowIfNull(_blockTree); @@ -223,7 +236,124 @@ private void OnForkChoiceUpdated(object? sender, IBlockTree.ForkChoice choice) TaskCompletionSource forkChoice = _forkChoice; _forkChoice = new TaskCompletionSource(); - forkChoice.TrySetResult(JsonSerializer.SerializeToUtf8Bytes(choice, JsonSerializerOptions.Web)); + var head = choice.Head; + Transaction[] txs = choice.Head.Transactions; + IReleaseSpec spec = _specProvider.GetSpec(choice.Head.Header); + var receipts = _receiptFinder.Get(choice.Head).Select((r, i) => new ReceiptForRpc(txs[i].Hash, r, txs[i].GetGasInfo(spec, choice.Head.Header))).ToArray(); + forkChoice.TrySetResult( + JsonSerializer.SerializeToUtf8Bytes( + new ForkData + { + Head = new BlockForWeb + { + ExtraData = head.ExtraData ?? [], + GasLimit = head.GasLimit, + GasUsed = head.GasUsed, + Hash = head.Hash ?? Hash256.Zero, + Beneficiary = head.Beneficiary ?? Address.Zero, + Number = head.Number, + Size = _blockDecoder.GetLength(head, RlpBehaviors.None), + Timestamp = head.Timestamp, + BaseFeePerGas = head.BaseFeePerGas, + BlobGasUsed = head.BlobGasUsed ?? 0, + ExcessBlobGas = head.ExcessBlobGas ?? 0, + Tx = head.Transactions.Select(t => new TransactionForWeb + { + Hash = t.Hash, + From = t.SenderAddress, + To = t.To, + TxType = (int)t.Type, + MaxPriorityFeePerGas = t.MaxPriorityFeePerGas, + MaxFeePerGas = t.MaxFeePerGas, + GasPrice = t.GasPrice, + GasLimit = t.GasLimit, + Nonce = t.Nonce, + Value = t.Value, + DataLength = t.DataLength, + Blobs = t.BlobVersionedHashes?.Length ?? 0 + }).ToArray(), + Receipts = receipts.Select(r => new ReceiptForWeb + { + GasUsed = r.GasUsed, + EffectiveGasPrice = r.EffectiveGasPrice ?? UInt256.Zero, + ContractAddress = r.ContractAddress, + Logs = r.Logs.Select(l => new LogEntryForWeb + { + Address = l.Address, + Data = l.Data, + Topics = l.Topics + }).ToArray(), + Status = r.Status, + BlobGasPrice = r.BlobGasPrice ?? UInt256.Zero, + BlobGasUsed = r.BlobGasUsed ?? 0, + }).ToArray() + }, + Safe = choice.Safe, + Finalized = choice.Finalized + }, + EthereumJsonSerializer.JsonOptions + ) + ); + } + + private class ForkData + { + public BlockForWeb Head { get; set; } + public long Safe { get; set; } + public long Finalized { get; set; } + } + + private class BlockForWeb + { + public byte[] ExtraData { get; set; } + public long GasLimit { get; set; } + public long GasUsed { get; set; } + public Hash256 Hash { get; set; } + public Address Beneficiary { get; set; } + public long Number { get; set; } + public int Size { get; set; } + public ulong Timestamp { get; set; } + public UInt256 BaseFeePerGas { get; set; } + public ulong BlobGasUsed { get; set; } + public ulong ExcessBlobGas { get; set; } + public TransactionForWeb[] Tx { get; set; } + public ReceiptForWeb[] Receipts { get; set; } + public ReceiptForWeb[] Withdrawals { get; set; } + } + private class ReceiptForWeb + { + public long GasUsed { get; set; } + public UInt256 EffectiveGasPrice { get; set; } + public Address? ContractAddress { get; set; } + public LogEntryForWeb[] Logs { get; set; } + public long Status { get; set; } + public UInt256 BlobGasPrice { get; set; } + public ulong BlobGasUsed { get; set; } + } + private class LogEntryForWeb + { + public Address Address { get; set; } + public byte[] Data { get; set; } + public Hash256[] Topics { get; set; } + } + private class TransactionForWeb + { + public Hash256 Hash { get; set; } + public Address From { get; set; } + public Address? To { get; set; } + public int TxType { get; set; } + public UInt256 MaxPriorityFeePerGas { get; set; } + public UInt256 MaxFeePerGas { get; set; } + public UInt256 GasPrice { get; set; } + public long GasLimit { get; set; } + public UInt256 Nonce { get; set; } + public UInt256 Value { get; set; } + public int DataLength { get; set; } + public int Blobs { get; set; } + } + private class WithdrawalForWeb + { + } TaskCompletionSource _log = new(); diff --git a/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj b/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj index b10e510a1a8..0bc795a2cdb 100644 --- a/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj +++ b/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj @@ -83,7 +83,9 @@ true + + @@ -99,7 +101,9 @@ + + diff --git a/src/Nethermind/Nethermind.Runner/scripts/app.ts b/src/Nethermind/Nethermind.Runner/scripts/app.ts index 19637fbcd73..93e390a14e2 100644 --- a/src/Nethermind/Nethermind.Runner/scripts/app.ts +++ b/src/Nethermind/Nethermind.Runner/scripts/app.ts @@ -5,8 +5,10 @@ import * as d3 from 'd3'; import Convert = require('ansi-to-html'); import { formatDuration } from './format'; import { sparkline, Datum } from './sparkline'; -import { NodeData, INode, TxPool, Processed, ForkChoice, System } from './types'; +import { NodeData, INode, TxPool, Processed, ForkChoice, System, Receipt, Transaction, TransactionReceipt } from './types'; import { TxPoolFlow } from './txPoolFlow'; +import { updateTreemap } from './treeMap' +import { formatUnixTimestamp, formatBytes, parseExtraData } from './utilities'; // Grab DOM elements const txPoolValue = document.getElementById('txPoolValue') as HTMLElement; @@ -44,6 +46,14 @@ const maxGas = document.getElementById('maxGas') as HTMLElement; const gasLimit = document.getElementById('gasLimit') as HTMLElement; const gasLimitDelta = document.getElementById('gasLimitDelta') as HTMLElement; +const blockExtraData = document.getElementById('blockExtraData') as HTMLElement; +const blockGasUsed = document.getElementById('blockGasUsed') as HTMLElement; +const blockGasLimit = document.getElementById('blockGasLimit') as HTMLElement; +const blockBlockSize = document.getElementById('blockBlockSize') as HTMLElement; +const blockTimestamp = document.getElementById('blockTimestamp') as HTMLElement; +const blockTransactions = document.getElementById('blockTransactions') as HTMLElement; +const blockBlobs = document.getElementById('blockBlobs') as HTMLElement; + const ansiConvert = new Convert(); // We reuse these arrays for the sparkline. The length = 60 means we store 60 historical points. @@ -90,6 +100,9 @@ let txPoolNodes: INode[] = null; * Main function to start polling data and updating the UI. */ function updateTxPool(txPool: TxPool) { + + if (txPool.pooledTx == 0) return; + const nowMs = performance.now(); const currentNow = nowMs / 1000; const diff = currentNow - lastNow; @@ -203,12 +216,48 @@ sse.addEventListener("forkChoice", (e) => { if (document.hidden) return; const data = JSON.parse(e.data) as ForkChoice; - updateText(headBlock, data.head.toFixed(0)); - updateText(safeBlock, data.safe.toFixed(0)); - updateText(finalizedBlock, data.finalized.toFixed(0)); - - updateText(safeBlockDelta, `(${(data.safe - data.head).toFixed(0)})`); - updateText(finalizedBlockDelta, `(${(data.finalized - data.head).toFixed(0)})`); + const number = parseInt(data.head.number, 16); + const safe = parseInt(data.safe, 16); + const finalized = parseInt(data.finalized, 16); + updateText(headBlock, number.toFixed(0)); + updateText(safeBlock, safe.toFixed(0)); + updateText(finalizedBlock, finalized.toFixed(0)); + + updateText(safeBlockDelta, `(${(safe - number).toFixed(0)})`); + updateText(finalizedBlockDelta, `(${(finalized - number).toFixed(0)})`); + + const block = data.head; + // Merge tx & receipts into a single array + // so each element has { key, size, colorValue, ... } etc. + // (One data item per transaction) + const mergedData: TransactionReceipt[] = block.tx.map((tx, i) => { + const receipt = block.receipts[i]; + return { order: i, ...tx, ...receipt }; + }); + + updateText(blockExtraData, parseExtraData(block.extraData)); + updateText(blockGasUsed, format(parseInt(block.gasUsed, 16))); + updateText(blockGasLimit, format(parseInt(block.gasLimit, 16))); + updateText(blockBlockSize, formatBytes(parseInt(block.size, 16))); + updateText(blockTimestamp, formatUnixTimestamp(parseInt(block.timestamp, 16))); + updateText(blockTransactions, format(block.tx.length)); + updateText(blockBlobs, format(block.tx.reduce((p, c) => p + c.blobs, 0))); + + // Update the D3 treemap with the merged data + updateTreemap( + document.getElementById("block"), // or d3.select("#treemap") + 200, + parseInt(data.head.gasLimit, 16), + mergedData, + // keyFn + d => d.hash, + // orderFn + d => d.order, + // sizeFn + d => parseInt(d.gasUsed, 16), + // colorFn + d => parseInt(d.effectiveGasPrice, 16) * parseInt(d.gasUsed, 16) + ); }); let maxCpuPercent = 0; diff --git a/src/Nethermind/Nethermind.Runner/scripts/treeMap.ts b/src/Nethermind/Nethermind.Runner/scripts/treeMap.ts new file mode 100644 index 00000000000..b493342bcc6 --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/scripts/treeMap.ts @@ -0,0 +1,322 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +import * as d3 from 'd3'; + +/** + * Builds or updates a treemap in a given element. + * + * @param element The HTMLElement container for the treemap + * @param size An object { width, height } for the overall treemap dimensions + * @param totalSize The capacity or "max size" to compare against the sum of sizes (e.g. gasLimit) + * @param data Flat array of items, each representing a leaf node + * @param keyFn Maps each item -> unique string (transaction hash, e.g.) + * @param groupFn Maps each item -> group name (for hierarchical grouping) + * @param sizeFn Maps each item -> numeric size (for area) + * @param colorFn Maps each item -> numeric value (for color scale) + */ +export function updateTreemap( + element: HTMLElement, + height: number, + totalSize: number, + data: T[], + keyFn: (d: T) => string, + orderFn: (d: T) => number, + sizeFn: (d: T) => number, + colorFn: (d: T) => number +) { + const width = window.innerWidth - (40 + 16); + const children = [...data.map((d) => ({ + name: keyFn(d), // leaf node name + item: d, // store original data + size: sizeFn(d) // numeric value for area + }))]; + const rootData = { name: "root", children }; + + // 3) Build a D3 hierarchy and sum by "size" + const root = d3.hierarchy(rootData) + .sum((node: any) => node.children ? 0 : sizeFn(node.data ? node.data.item : node.item )) + .sort((node: any) => node.children ? 0 : orderFn(node.data ? node.data.item : node.item )); + + // 4) Figure out how much "used" size we have relative to the totalSize + const used = root.value ?? 0; + // The fraction of capacity used + const ratio = used > 0 ? used / totalSize : 0; + // We'll clamp so that if used > totalSize, the treemap still fits. + const usedWidth = Math.min(width, width * ratio); + + // 5) Apply a treemap layout to the hierarchy, + // giving it [usedWidth, height] so it will only fill proportionally. + d3.treemap() + .size([usedWidth, height - 1]) + .round(true) + .tile(d3.treemapSquarify.ratio(1)) + .paddingOuter(0.5) + .paddingInner(2)(root); + + // 7) Define a numeric scale for color strength from colorFn + // (e.g. from min effectiveGasPrice to max) + // + // 1) Compute min/max from your data + const [minVal, maxVal] = d3.extent(data, colorFn); + + // 2) For a log scale, make sure your minimum is > 0. + // If minVal <= 0, you need to clamp or shift up to a safe positive value. + // For example: + const safeMin = minVal && minVal > 0 ? minVal : 1e-6; // pick an epsilon for 0 or negative + const safeMax = maxVal || 1; // fallback if maxVal is null/undefined + + // 4) Build a scaleSequentialLog using that interpolator + // This will map [safeMin ... safeMax] => [0..1], + // then feed that t ∈ [0..1] into your custom colorInterpolator. + const colorScale = d3 + .scaleSequentialLog(d3.interpolateCool) + .domain([safeMin, safeMax]); + + // 5) Finally, use it in your getColor(...) helper: + function getColor(item: T): string { + const value = colorFn(item); + // If value <= 0, clamp or shift it so we don't break the log scale: + const safeValue = value > 0 ? value : 1e-6; + return colorScale(safeValue); + } + + // 7) Get a D3 selection for the element + let svg = d3.select(element).select('svg'); + if (svg.empty()) { + svg = d3.select(element) + .append('svg') + .attr('width', width) + .attr('height', height) + .attr('viewBox', [0, 0, width, height]) as any; + + const defs = svg.append("defs"); + + const pattern = defs + .append("pattern") + .attr("id", "unusedStripes") + .attr("patternUnits", "userSpaceOnUse") + .attr("width", 8) + .attr("height", 8) + + pattern + .append("rect") + .attr("class", "pattern-bg") + .attr("width", 8) + .attr("height", 8) + .attr("fill", "#444"); + + pattern + .append("path") + .attr("d", "M0,0 l8,8") + .attr("stroke", "#000") + .attr("stroke-width", 1); + + svg.append('rect') + .attr("class", "unused") + // fill with our diagonal stripes pattern + .attr("fill", "url(#unusedStripes)") + .attr("opacity", 1) + .attr("width", width) + .attr("height", height) + .attr("stroke", "#fff") + .attr("stroke-width", 1); + + // Arrow at the start + defs + .append("marker") + .attr("id", "arrowStart") + .attr("markerWidth", 10) + .attr("markerHeight", 10) + .attr("refX", 5) // x reference point where arrow is "anchored" + .attr("refY", 5) // y reference point (vertical center) + .attr("orient", "auto") // orient automatically along the line + .attr("markerUnits", "strokeWidth") // scale arrow based on line width + .append("path") + .attr("d", "M10,0 L0,5 L10,10") // a simple arrow shape + .attr("fill", "#ccc"); + + // Arrow at the end + defs + .append("marker") + .attr("id", "arrowEnd") + .attr("markerWidth", 10) + .attr("markerHeight", 10) + .attr("refX", 5) + .attr("refY", 5) + .attr("orient", "auto") + .attr("markerUnits", "strokeWidth") + .append("path") + .attr("d", "M0,0 L10,5 L0,10") // mirrored arrow shape + .attr("fill", "#ccc"); + } + + svg + .attr('width', width) + .attr('height', height) + .attr('viewBox', [0, 0, width, height]); + + svg.selectAll('rect.unused') + .attr("width", width); + + // 8) We'll render leaf nodes only. Each leaf has { name, item, size } in d.data. + const leaves = root.leaves(); + + const node = svg + .selectAll>("g.node") + .data(leaves, (d) => d.data.name); // key by the leaf's name + + // 11) Use .join(...) with transitions + node.join( + // ENTER + (enter) => { + const gEnter = enter + .append("g") + .attr("class", "node") + // Start each group at final XY but with zero size + opacity=0 + .attr("transform", (d: any) => `translate(${d.x0},${d.y0})`) + .attr("opacity", 0); + + gEnter + .append("rect") + .attr("stroke", "#000") + .attr("stroke-width", 0.5) + .attr("width", 0) + .attr("height", 0) + .attr("fill", (d: any) => getColor(d.data.item)); + + // Transition in + gEnter + .transition() + .duration(600) + .attr("opacity", 1); + + gEnter + .select("rect") + .transition() + .duration(600) + .attr("width", (d: any) => d.x1 - d.x0) + .attr("height", (d: any) => d.y1 - d.y0); + + return gEnter; + }, + // UPDATE + (update) => { + update + .transition() + .duration(600) + .attr("transform", (d: any) => `translate(${d.x0},${d.y0})`) + .attr("opacity", 1); + + update + .select("rect") + .transition() + .duration(600) + .attr("width", (d: any) => d.x1 - d.x0) + .attr("height", (d: any) => d.y1 - d.y0) + .attr("fill", (d: any) => getColor(d.data.item)); + + return update; + }, + // EXIT + (exit) => { + exit + .transition() + .duration(600) + .attr("opacity", 0) + .remove(); + } + ); + + + // + // 10) Draw a border or label for the "unused" area if used < totalSize + const leftoverRatio = (totalSize - used) / totalSize; + const showLeftoverLabel = leftoverRatio >= 0.1; // i.e. >= 10% + + // If leftoverRatio >= 0.1, we show a label with line + // The object below can carry leftover ratio & geometry (x, w). + const leftoverData = showLeftoverLabel + ? [ + { + key: "unused-label", + x: usedWidth, // the X where "unused" area starts + w: width - usedWidth, // how wide the leftover region is + ratio: leftoverRatio + }, + ] + : []; + + svg + .selectAll("line.unused-arrow") + .data(leftoverData, (d) => d.key) + .join( + (enter) => + enter + .append("line") + .attr("class", "unused-arrow") + // Position the line horizontally from x1..x2 + .attr("x1", (d) => d.x + 10) // slight offset from the left edge + .attr("x2", (d) => d.x + d.w - 10) // slight offset from the right edge + .attr("y1", 170) // pick your desired vertical offset + .attr("y2", 170) + .attr("stroke", "#ccc") + .attr("stroke-width", 2) + .attr("marker-start", "url(#arrowStart)") + .attr("marker-end", "url(#arrowEnd)") + .attr("opacity", 0) + .call((enterSel) => + enterSel + .transition() + .duration(600) + .attr("opacity", 1) + ), + (update) => + update + .transition() + .duration(600) + .attr("x1", (d) => d.x + 10) + .attr("x2", (d) => d.x + d.w - 10), + (exit) => + exit + .transition() + .duration(600) + .attr("opacity", 0) + .remove() + ); + + svg + .selectAll("text.unused-label") + .data(leftoverData, (d) => d.key) + .join( + (enter) => + enter + .append("text") + .attr("class", "unused-label") + .attr("x", (d) => d.x + d.w / 2) + .attr("y", 155) // slightly above the line (which is at y=40) + .attr("fill", "#ccc") + .attr("font-size", 14) + .attr("text-anchor", "middle") + .attr("opacity", 0) + .text((d) => `${(d.ratio * 100).toFixed(1)}% available`) + .call((enterSel) => + enterSel + .transition() + .duration(600) + .attr("opacity", 1) + ), + (update) => + update + .transition() + .duration(600) + .attr("x", (d) => d.x + d.w / 2) + .text((d) => `${(d.ratio * 100).toFixed(1)}% available`), + (exit) => + exit + .transition() + .duration(600) + .attr("opacity", 0) + .remove() + ); +} diff --git a/src/Nethermind/Nethermind.Runner/scripts/txPoolFlow.ts b/src/Nethermind/Nethermind.Runner/scripts/txPoolFlow.ts index 65b80e4f94b..0727aec3ae4 100644 --- a/src/Nethermind/Nethermind.Runner/scripts/txPoolFlow.ts +++ b/src/Nethermind/Nethermind.Runner/scripts/txPoolFlow.ts @@ -37,9 +37,9 @@ export class TxPoolFlow { constructor(container: string) { this.svg = d3.select(container) .append('svg') - .attr('width', this.width) + .attr('width', window.innerWidth) .attr('height', this.height) - .attr('viewBox', [0, 0, this.width, this.height]) + .attr('viewBox', [0, 0, window.innerWidth, this.height]) .style('max-width', '100%') .style('height', 'auto'); this.defs = this.svg.append('defs'); @@ -121,6 +121,12 @@ export class TxPoolFlow { const filteredLinks: ILink[] = []; const usedNodes: Record = {}; + this.width = window.innerWidth - (40 + 16); + this.svg + .attr('width', this.width) + .attr('height', this.height) + .attr('viewBox', [0, 0, this.width, this.height]); + for (const link of data.links) { if (link.value > 0) { filteredLinks.push(link); diff --git a/src/Nethermind/Nethermind.Runner/scripts/types.ts b/src/Nethermind/Nethermind.Runner/scripts/types.ts index 8b3d977673e..1f35e0a818b 100644 --- a/src/Nethermind/Nethermind.Runner/scripts/types.ts +++ b/src/Nethermind/Nethermind.Runner/scripts/types.ts @@ -44,13 +44,61 @@ export interface Processed } export interface ForkChoice { - head: number; - safe: number; - finalized: number; + head: Block; + safe: string; + finalized: string; +} + +export interface Block { + extraData: string; + gasLimit: string; + gasUsed: string; + hash: string; + beneficiary: string; + number: string; + size: string; + timestamp: string; + baseFeePerGas: string; + blobGasUsed: string; + excessBlobGas: string; + tx: Transaction[]; // matched to receipts 1:1 + receipts: Receipt[]; // matched to tx 1:1 +} +export interface Transaction { + hash: string; + from: string; + to: string; + txType: string; + maxPriorityFeePerGas: string; + maxFeePerGas: string; + gasPrice: string; + gasLimit: string; + nonce: string; + value: string; + dataLength: number; + blobs: number; +} +export interface Receipt { + gasUsed: string; + effectiveGasPrice: string; + contractAddress: string; + blobGasPrice: string; + blobGasUsed: string; + logs: Log[]; + status: string; +} + +export interface TransactionReceipt extends Transaction, Receipt { + order: number +} +export interface Log { + address: string; + data: string; + topics: string[]; } export interface System { - uptime: number, + uptime: number; userPercent: number; privilegedPercent: number; workingSet: number; diff --git a/src/Nethermind/Nethermind.Runner/scripts/utilities.ts b/src/Nethermind/Nethermind.Runner/scripts/utilities.ts new file mode 100644 index 00000000000..a598a61e25d --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/scripts/utilities.ts @@ -0,0 +1,270 @@ + + +const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + +export function formatUnixTimestamp(timestampInSeconds: number): string { + // Convert the Unix timestamp (seconds) to milliseconds + const date = new Date(timestampInSeconds * 1000); + + // Prepare day, month, year, hours, minutes, seconds + const day = String(date.getDate()).padStart(2, '0'); + const month = months[date.getMonth()]; + const year = date.getFullYear(); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + + // Return the formatted date string + // Example: "09 Feb 2025 13:45:00" + return `${day} ${month} ${year} ${hours}:${minutes}:${seconds}`; +} + +const units = ["bytes", "kB", "MB", "GB", "TB", "PB"]; + +export function formatBytes(bytes: number): string { + // Handle negative or NaN gracefully (could also throw an error if needed) + if (!Number.isFinite(bytes) || bytes < 0) { + return "0 bytes"; + } + + // Start with the base unit + let unitIndex = 0; + let value = bytes; + + // Divide down until the value is < 1000 or we reach the last unit + while (value >= 1000 && unitIndex < units.length - 1) { + value /= 1000; + unitIndex++; + } + + // Round to one decimal place + let rounded = parseFloat(value.toFixed(1)); + + // If rounding gave us 1000.0, move to the next unit (if possible) + if (rounded === 1000 && unitIndex < units.length - 1) { + rounded = 1.0; + unitIndex++; + } + + // If the rounded value is actually an integer, we may want to show + // no decimal places (e.g. "1 kB" instead of "1.0 kB"). + // Adjust to your preference. Here we drop the .0 if integer: + const roundedStr = + rounded % 1 === 0 ? `${rounded.toFixed(0)}` : `${rounded.toFixed(1)}`; + + return `${roundedStr} ${units[unitIndex]}`; +} + +/** + * Converts a hex string (with "0x" prefix) into a human-readable string: + * - If the data decodes to (mostly) valid text, returns "Extra Data: ". + * - Otherwise, returns "Hex: 0x". + * + * @param extraData A hex string starting with "0x". + * @returns A string describing either the UTF-8 interpretation or the raw hex. + */ +export function parseExtraData(extraData: string | null | undefined): string { + if (!extraData || extraData === "0x") { + return "0x"; + } + + // Remove the "0x" prefix if present + let hex = extraData.startsWith("0x") + ? extraData.slice(2) + : extraData; + + // If there is nothing after removing "0x", return "0x" + if (hex.length === 0) { + return "0x"; + } + + // Convert hex -> bytes + const bytes = hexStringToUint8Array(hex); + + // Use the "toCleanUtf8String" logic + const data = toCleanUtf8String(bytes); + + // The C# snippet's comment says: + // "If the cleaned text is less than half length of input size, output it as hex, else output the text." + // The original code in the snippet has a minor mismatch in the condition, but we’ll follow the comment: + if (data.length > (bytes.length / 2)) { + // It's "mostly" valid text + return data; + } else { + // It's "mostly" not decodable text => show hex + return `0x${hex}`; + } +} + +/** + * This function mimics the C# code: + * - it only processes if bytes.length <= 32, + * - decodes UTF-8 codepoints individually, + * - skips control characters (potentially inserting a single space if they've appeared after valid text), + * - stops on incomplete sequences, + * - and returns the final string. + */ +function toCleanUtf8String(bytes: Uint8Array): string { + // The C# code short-circuits if empty or more than 32 bytes + if (bytes.length === 0 || bytes.length > 32) { + return ""; + } + + let resultChars: string[] = []; + let index = 0; + let hasValidContent = false; + let shouldAddSpace = false; + + while (index < bytes.length) { + // Decode next code point from UTF-8 + const { codePoint, consumed, status } = decodeUtf8Rune(bytes, index); + + if (status === "done") { + // Check if codePoint is a control character + if (!isControlCharacter(codePoint)) { + // If we previously skipped a control char and wanted to insert a space, do so now + if (shouldAddSpace) { + resultChars.push(" "); + shouldAddSpace = false; + } + // Add the decoded character + resultChars.push(String.fromCodePoint(codePoint)); + hasValidContent = true; + } else { + // We encountered a control char, we mark that maybe we should insert + // a space if we have had valid content + shouldAddSpace = shouldAddSpace || hasValidContent; + } + index += consumed; + } else if (status === "needMoreData") { + // Incomplete sequence at the end => break + break; + } else { + // status === "invalid" + // Skip invalid byte, but set up for possible space + shouldAddSpace = shouldAddSpace || hasValidContent; + index += 1; + } + } + + return resultChars.join(""); +} + +/** + * Attempts to decode one UTF-8 code point from `bytes` starting at `offset`. + * + * Returns an object: + * - codePoint: number (the decoded Unicode code point if successful) + * - consumed: number (how many bytes were consumed) + * - status: "done" | "needMoreData" | "invalid" + */ +function decodeUtf8Rune(bytes: Uint8Array, offset: number): { + codePoint: number; + consumed: number; + status: "done" | "needMoreData" | "invalid"; +} { + if (offset >= bytes.length) { + return { codePoint: 0, consumed: 0, status: "needMoreData" }; + } + + const b0 = bytes[offset]; + + // 1-byte sequence + if ((b0 & 0x80) === 0) { + // ASCII + return { codePoint: b0, consumed: 1, status: "done" }; + } + + // For multi-byte, ensure we have enough data + // and check validity according to UTF-8 rules + if ((b0 & 0xe0) === 0xc0) { + // 2-byte sequence: 110xxxxx 10xxxxxx + if (offset + 1 >= bytes.length) { + return { codePoint: 0, consumed: 0, status: "needMoreData" }; + } + const b1 = bytes[offset + 1]; + if ((b1 & 0xc0) !== 0x80 || (b0 & 0xfe) === 0xc0) { + // Overlong or invalid continuation + return { codePoint: 0, consumed: 0, status: "invalid" }; + } + const codePoint = ((b0 & 0x1f) << 6) | (b1 & 0x3f); + return { codePoint, consumed: 2, status: "done" }; + } + + if ((b0 & 0xf0) === 0xe0) { + // 3-byte sequence: 1110xxxx 10xxxxxx 10xxxxxx + if (offset + 2 >= bytes.length) { + return { codePoint: 0, consumed: 0, status: "needMoreData" }; + } + const b1 = bytes[offset + 1]; + const b2 = bytes[offset + 2]; + if (((b1 & 0xc0) !== 0x80) || ((b2 & 0xc0) !== 0x80)) { + return { codePoint: 0, consumed: 0, status: "invalid" }; + } + // Check for overlong forms + const cp = ((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f); + // Overlong or invalid + if ((cp >= 0xd800 && cp <= 0xdfff) || cp === 0) { + return { codePoint: 0, consumed: 0, status: "invalid" }; + } + return { codePoint: cp, consumed: 3, status: "done" }; + } + + if ((b0 & 0xf8) === 0xf0) { + // 4-byte sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if (offset + 3 >= bytes.length) { + return { codePoint: 0, consumed: 0, status: "needMoreData" }; + } + const b1 = bytes[offset + 1]; + const b2 = bytes[offset + 2]; + const b3 = bytes[offset + 3]; + if (((b1 & 0xc0) !== 0x80) || + ((b2 & 0xc0) !== 0x80) || + ((b3 & 0xc0) !== 0x80)) { + return { codePoint: 0, consumed: 0, status: "invalid" }; + } + const cp = ((b0 & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f); + // Check for valid range (UTF-8 up to U+10FFFF) + if (cp > 0x10ffff || cp < 0x10000) { + // Could be overlong or out of range + return { codePoint: 0, consumed: 0, status: "invalid" }; + } + return { codePoint: cp, consumed: 4, status: "done" }; + } + + // If we reach here, it's invalid + return { codePoint: 0, consumed: 0, status: "invalid" }; +} + +/** + * Check if the code point is a "control character" that we want to skip. + * + * You can adapt this check if you only want to skip ASCII controls < 0x20, etc. + */ +function isControlCharacter(codePoint: number): boolean { + // This is a simple ASCII-based check. + // The .NET version of Rune.IsControl might do more comprehensive checks. + if (codePoint < 0x20) { + return true; // Basic control range + } + if (codePoint >= 0x7f && codePoint < 0xa0) { + return true; // DEL and extended ASCII control range + } + return false; +} + +/** + * Converts a hex string (without "0x") to a Uint8Array. + */ +function hexStringToUint8Array(hex: string): Uint8Array { + if (hex.length === 0 || hex.length % 2 !== 0) { + // In real code, handle error or return empty array if invalid hex + return new Uint8Array(); + } + + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); + } + return bytes; +} diff --git a/src/Nethermind/Nethermind.Runner/wwwroot/index.html b/src/Nethermind/Nethermind.Runner/wwwroot/index.html index e74a63598a1..847188babf3 100644 --- a/src/Nethermind/Nethermind.Runner/wwwroot/index.html +++ b/src/Nethermind/Nethermind.Runner/wwwroot/index.html @@ -85,8 +85,43 @@
Max: MB
+
+
Lastest Block
+
Gas Target
+
+
Extra Data: 0x
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Timestamp0
Gas Limit0
Gas Used0
Block Size0
Transactions0
Blobs0
+
+
+
-
Transaction Flows
+
Incoming Transactions
From b531de3521d4a9e0c979bebff7cdcedd686bb6e3 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 13 Feb 2025 05:23:02 +0000 Subject: [PATCH 5/5] formatting --- src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs b/src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs index 395275ac138..5471b0a8482 100644 --- a/src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs +++ b/src/Nethermind/Nethermind.Runner/Monitoring/DataFeed.cs @@ -316,8 +316,8 @@ private class BlockForWeb public UInt256 BaseFeePerGas { get; set; } public ulong BlobGasUsed { get; set; } public ulong ExcessBlobGas { get; set; } - public TransactionForWeb[] Tx { get; set; } - public ReceiptForWeb[] Receipts { get; set; } + public TransactionForWeb[] Tx { get; set; } + public ReceiptForWeb[] Receipts { get; set; } public ReceiptForWeb[] Withdrawals { get; set; } } private class ReceiptForWeb