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 115be2bbcd1..39d24ff2266 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 c5c9bc5d747..3a2e8c08a2b 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..19637fbcd73 --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/scripts/app.ts @@ -0,0 +1,278 @@ +// 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 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 + 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) { + const nowMs = performance.now(); + const currentNow = nowMs / 1000; + const diff = currentNow - lastNow; + + // 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; + + if (lastNow !== 0) { + 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 }); + } + + + 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 + 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) => { + 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; + } + + const now = performance.now(); + addCapped(seriesTotalCpu, { t: now, v: data.userPercent + data.privilegedPercent }); + addCapped(seriesMemory, { t: now, v: memoryMb }); + + if (document.hidden) return; + + updateText(upTime, formatDuration(data.uptime)); + + updateText(cpuTime, formatDec(cpuPercent)); + updateText(maxCpuTime, formatDec(maxCpuPercent)); + sparkline(sparkCpu, seriesTotalCpu, 300, 100, 60); + + updateText(memory, format(memoryMb)); + updateText(maxMemory, format(maxMemoryMb)); + sparkline(sparkMemory, seriesMemory, 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..8afe5edb94c --- /dev/null +++ b/src/Nethermind/Nethermind.Runner/scripts/sparkline.ts @@ -0,0 +1,131 @@ +// 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 60). + */ +export function sparkline( + element: HTMLElement, + data: 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 + // + 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 00000000000..bce59662e44 Binary files /dev/null and b/src/Nethermind/Nethermind.Runner/wwwroot/favicon.webp differ diff --git a/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-mono-latin-ext.woff2 b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-mono-latin-ext.woff2 new file mode 100644 index 00000000000..7accc129f76 Binary files /dev/null and b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-mono-latin-ext.woff2 differ diff --git a/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-mono-latin.woff2 b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-mono-latin.woff2 new file mode 100644 index 00000000000..f723280b080 Binary files /dev/null and b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-mono-latin.woff2 differ diff --git a/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-sans-latin-ext.woff2 b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-sans-latin-ext.woff2 new file mode 100644 index 00000000000..373b73afdd8 Binary files /dev/null and b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-sans-latin-ext.woff2 differ 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 00000000000..5a77bb3dd5e Binary files /dev/null and b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/dm-sans-latin.woff2 differ diff --git a/src/Nethermind/Nethermind.Runner/wwwroot/fonts/exo-latin-ext.woff2 b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/exo-latin-ext.woff2 new file mode 100644 index 00000000000..e1631f4f425 Binary files /dev/null and b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/exo-latin-ext.woff2 differ diff --git a/src/Nethermind/Nethermind.Runner/wwwroot/fonts/exo-latin.woff2 b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/exo-latin.woff2 new file mode 100644 index 00000000000..17cd2fdb919 Binary files /dev/null and b/src/Nethermind/Nethermind.Runner/wwwroot/fonts/exo-latin.woff2 differ 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 5bce5d6ac21..f1088668e08 100644 --- a/src/Nethermind/Nethermind.TxPool/TxPool.cs +++ b/src/Nethermind/Nethermind.TxPool/TxPool.cs @@ -276,6 +276,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++) @@ -313,6 +315,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,28 @@ private void RemoveProcessedTransactions(Block block) } } + if (blockTx.Type == TxType.SetCode) + { + 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 +386,9 @@ private void RemoveProcessedTransactions(Block block) Metrics.Eip7702TransactionsInBlock = eip7702Txs; Metrics.BlobTransactionsInBlock = blobTxs; Metrics.BlobsInBlock = blobs; + + Metrics.TransactionsSourcedPrivateOrderflow += notInMempoool; + Metrics.TransactionsSourcedMemPool += transactionsInBlock - notInMempoool; } } @@ -918,21 +938,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