diff --git a/vms/evm/sync/statesynctest/test_stats.go b/vms/evm/sync/statesynctest/test_stats.go new file mode 100644 index 000000000000..6da4fd2b163c --- /dev/null +++ b/vms/evm/sync/statesynctest/test_stats.go @@ -0,0 +1,234 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package statesynctest + +import ( + "sync" + "time" + + "github.com/ava-labs/avalanchego/vms/evm/sync/stats" +) + +var _ stats.HandlerStats = (*TestHandlerStats)(nil) + +// TestHandlerStats is test for capturing and asserting on handler metrics in test +type TestHandlerStats struct { + lock sync.Mutex + + BlockRequestCount, + MissingBlockHashCount, + BlocksReturnedSum uint32 + BlockRequestProcessingTimeSum time.Duration + + CodeRequestCount, + MissingCodeHashCount, + TooManyHashesRequested, + DuplicateHashesRequested, + CodeBytesReturnedSum uint32 + CodeReadTimeSum time.Duration + + LeafsRequestCount, + InvalidLeafsRequestCount, + LeafsReturnedSum, + MissingRootCount, + TrieErrorCount, + ProofErrorCount, + SnapshotReadErrorCount, + SnapshotReadAttemptCount, + SnapshotReadSuccessCount, + SnapshotSegmentValidCount, + SnapshotSegmentInvalidCount uint32 + ProofValsReturned int64 + LeafsReadTime, + SnapshotReadTime, + GenerateRangeProofTime, + LeafRequestProcessingTimeSum time.Duration +} + +func (m *TestHandlerStats) Reset() { + m.lock.Lock() + defer m.lock.Unlock() + m.BlockRequestCount = 0 + m.MissingBlockHashCount = 0 + m.BlocksReturnedSum = 0 + m.BlockRequestProcessingTimeSum = 0 + m.CodeRequestCount = 0 + m.MissingCodeHashCount = 0 + m.TooManyHashesRequested = 0 + m.DuplicateHashesRequested = 0 + m.CodeBytesReturnedSum = 0 + m.CodeReadTimeSum = 0 + m.LeafsRequestCount = 0 + m.InvalidLeafsRequestCount = 0 + m.LeafsReturnedSum = 0 + m.MissingRootCount = 0 + m.TrieErrorCount = 0 + m.ProofErrorCount = 0 + m.SnapshotReadErrorCount = 0 + m.SnapshotReadAttemptCount = 0 + m.SnapshotReadSuccessCount = 0 + m.SnapshotSegmentValidCount = 0 + m.SnapshotSegmentInvalidCount = 0 + m.ProofValsReturned = 0 + m.LeafsReadTime = 0 + m.SnapshotReadTime = 0 + m.GenerateRangeProofTime = 0 + m.LeafRequestProcessingTimeSum = 0 +} + +func (m *TestHandlerStats) IncBlockRequest() { + m.lock.Lock() + defer m.lock.Unlock() + m.BlockRequestCount++ +} + +func (m *TestHandlerStats) IncMissingBlockHash() { + m.lock.Lock() + defer m.lock.Unlock() + m.MissingBlockHashCount++ +} + +func (m *TestHandlerStats) UpdateBlocksReturned(num uint16) { + m.lock.Lock() + defer m.lock.Unlock() + m.BlocksReturnedSum += uint32(num) +} + +func (m *TestHandlerStats) UpdateBlockRequestProcessingTime(duration time.Duration) { + m.lock.Lock() + defer m.lock.Unlock() + m.BlockRequestProcessingTimeSum += duration +} + +func (m *TestHandlerStats) IncCodeRequest() { + m.lock.Lock() + defer m.lock.Unlock() + m.CodeRequestCount++ +} + +func (m *TestHandlerStats) IncMissingCodeHash() { + m.lock.Lock() + defer m.lock.Unlock() + m.MissingCodeHashCount++ +} + +func (m *TestHandlerStats) IncTooManyHashesRequested() { + m.lock.Lock() + defer m.lock.Unlock() + m.TooManyHashesRequested++ +} + +func (m *TestHandlerStats) IncDuplicateHashesRequested() { + m.lock.Lock() + defer m.lock.Unlock() + m.DuplicateHashesRequested++ +} + +func (m *TestHandlerStats) UpdateCodeReadTime(duration time.Duration) { + m.lock.Lock() + defer m.lock.Unlock() + m.CodeReadTimeSum += duration +} + +func (m *TestHandlerStats) UpdateCodeBytesReturned(bytes uint32) { + m.lock.Lock() + defer m.lock.Unlock() + m.CodeBytesReturnedSum += bytes +} + +func (m *TestHandlerStats) IncLeafsRequest() { + m.lock.Lock() + defer m.lock.Unlock() + m.LeafsRequestCount++ +} + +func (m *TestHandlerStats) IncInvalidLeafsRequest() { + m.lock.Lock() + defer m.lock.Unlock() + m.InvalidLeafsRequestCount++ +} + +func (m *TestHandlerStats) UpdateLeafsReturned(numLeafs uint16) { + m.lock.Lock() + defer m.lock.Unlock() + m.LeafsReturnedSum += uint32(numLeafs) +} + +func (m *TestHandlerStats) UpdateLeafsRequestProcessingTime(duration time.Duration) { + m.lock.Lock() + defer m.lock.Unlock() + m.LeafRequestProcessingTimeSum += duration +} + +func (m *TestHandlerStats) UpdateReadLeafsTime(duration time.Duration) { + m.lock.Lock() + defer m.lock.Unlock() + m.LeafsReadTime += duration +} + +func (m *TestHandlerStats) UpdateGenerateRangeProofTime(duration time.Duration) { + m.lock.Lock() + defer m.lock.Unlock() + m.GenerateRangeProofTime += duration +} + +func (m *TestHandlerStats) UpdateSnapshotReadTime(duration time.Duration) { + m.lock.Lock() + defer m.lock.Unlock() + m.SnapshotReadTime += duration +} + +func (m *TestHandlerStats) UpdateRangeProofValsReturned(numProofVals int64) { + m.lock.Lock() + defer m.lock.Unlock() + m.ProofValsReturned += numProofVals +} + +func (m *TestHandlerStats) IncMissingRoot() { + m.lock.Lock() + defer m.lock.Unlock() + m.MissingRootCount++ +} + +func (m *TestHandlerStats) IncTrieError() { + m.lock.Lock() + defer m.lock.Unlock() + m.TrieErrorCount++ +} + +func (m *TestHandlerStats) IncProofError() { + m.lock.Lock() + defer m.lock.Unlock() + m.ProofErrorCount++ +} + +func (m *TestHandlerStats) IncSnapshotReadError() { + m.lock.Lock() + defer m.lock.Unlock() + m.SnapshotReadErrorCount++ +} + +func (m *TestHandlerStats) IncSnapshotReadAttempt() { + m.lock.Lock() + defer m.lock.Unlock() + m.SnapshotReadAttemptCount++ +} + +func (m *TestHandlerStats) IncSnapshotReadSuccess() { + m.lock.Lock() + defer m.lock.Unlock() + m.SnapshotReadSuccessCount++ +} + +func (m *TestHandlerStats) IncSnapshotSegmentValid() { + m.lock.Lock() + defer m.lock.Unlock() + m.SnapshotSegmentValidCount++ +} + +func (m *TestHandlerStats) IncSnapshotSegmentInvalid() { + m.lock.Lock() + defer m.lock.Unlock() + m.SnapshotSegmentInvalidCount++ +} diff --git a/vms/evm/sync/stats/handler_stats.go b/vms/evm/sync/stats/handler_stats.go new file mode 100644 index 000000000000..2a03ca9d766c --- /dev/null +++ b/vms/evm/sync/stats/handler_stats.go @@ -0,0 +1,244 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package stats + +import ( + "time" + + "github.com/ava-labs/libevm/metrics" +) + +// HandlerStats reports prometheus metrics for the state sync handlers +type HandlerStats interface { + BlockRequestHandlerStats + CodeRequestHandlerStats + LeafsRequestHandlerStats +} + +type BlockRequestHandlerStats interface { + IncBlockRequest() + IncMissingBlockHash() + UpdateBlocksReturned(num uint16) + UpdateBlockRequestProcessingTime(duration time.Duration) +} + +type CodeRequestHandlerStats interface { + IncCodeRequest() + IncMissingCodeHash() + IncTooManyHashesRequested() + IncDuplicateHashesRequested() + UpdateCodeReadTime(duration time.Duration) + UpdateCodeBytesReturned(bytes uint32) +} + +type LeafsRequestHandlerStats interface { + IncLeafsRequest() + IncInvalidLeafsRequest() + UpdateLeafsReturned(numLeafs uint16) + UpdateLeafsRequestProcessingTime(duration time.Duration) + UpdateReadLeafsTime(duration time.Duration) + UpdateSnapshotReadTime(duration time.Duration) + UpdateGenerateRangeProofTime(duration time.Duration) + UpdateRangeProofValsReturned(numProofVals int64) + IncMissingRoot() + IncTrieError() + IncProofError() + IncSnapshotReadError() + IncSnapshotReadAttempt() + IncSnapshotReadSuccess() + IncSnapshotSegmentValid() + IncSnapshotSegmentInvalid() +} + +type handlerStats struct { + // BlockRequestHandler metrics + blockRequest metrics.Counter + missingBlockHash metrics.Counter + blocksReturned metrics.Histogram + blockRequestProcessingTime metrics.Timer + + // CodeRequestHandler stats + codeRequest metrics.Counter + missingCodeHash metrics.Counter + tooManyHashesRequested metrics.Counter + duplicateHashesRequested metrics.Counter + codeBytesReturned metrics.Histogram + codeReadDuration metrics.Timer + + // LeafsRequestHandler stats + leafsRequest metrics.Counter + invalidLeafsRequest metrics.Counter + leafsReturned metrics.Histogram + leafsRequestProcessingTime metrics.Timer + leafsReadTime metrics.Timer + snapshotReadTime metrics.Timer + generateRangeProofTime metrics.Timer + proofValsReturned metrics.Histogram + missingRoot metrics.Counter + trieError metrics.Counter + proofError metrics.Counter + snapshotReadError metrics.Counter + snapshotReadAttempt metrics.Counter + snapshotReadSuccess metrics.Counter + snapshotSegmentValid metrics.Counter + snapshotSegmentInvalid metrics.Counter +} + +func (h *handlerStats) IncBlockRequest() { + h.blockRequest.Inc(1) +} + +func (h *handlerStats) IncMissingBlockHash() { + h.missingBlockHash.Inc(1) +} + +func (h *handlerStats) UpdateBlocksReturned(num uint16) { + h.blocksReturned.Update(int64(num)) +} + +func (h *handlerStats) UpdateBlockRequestProcessingTime(duration time.Duration) { + h.blockRequestProcessingTime.Update(duration) +} + +func (h *handlerStats) IncCodeRequest() { + h.codeRequest.Inc(1) +} + +func (h *handlerStats) IncMissingCodeHash() { + h.missingCodeHash.Inc(1) +} + +func (h *handlerStats) IncTooManyHashesRequested() { + h.tooManyHashesRequested.Inc(1) +} + +func (h *handlerStats) IncDuplicateHashesRequested() { + h.duplicateHashesRequested.Inc(1) +} + +func (h *handlerStats) UpdateCodeReadTime(duration time.Duration) { + h.codeReadDuration.Update(duration) +} + +func (h *handlerStats) UpdateCodeBytesReturned(bytesLen uint32) { + h.codeBytesReturned.Update(int64(bytesLen)) +} + +func (h *handlerStats) IncLeafsRequest() { + h.leafsRequest.Inc(1) +} + +func (h *handlerStats) IncInvalidLeafsRequest() { + h.invalidLeafsRequest.Inc(1) +} + +func (h *handlerStats) UpdateLeafsRequestProcessingTime(duration time.Duration) { + h.leafsRequestProcessingTime.Update(duration) +} + +func (h *handlerStats) UpdateLeafsReturned(numLeafs uint16) { + h.leafsReturned.Update(int64(numLeafs)) +} + +func (h *handlerStats) UpdateReadLeafsTime(duration time.Duration) { + h.leafsReadTime.Update(duration) +} + +func (h *handlerStats) UpdateSnapshotReadTime(duration time.Duration) { + h.snapshotReadTime.Update(duration) +} + +func (h *handlerStats) UpdateGenerateRangeProofTime(duration time.Duration) { + h.generateRangeProofTime.Update(duration) +} + +func (h *handlerStats) UpdateRangeProofValsReturned(numProofVals int64) { + h.proofValsReturned.Update(numProofVals) +} + +func (h *handlerStats) IncMissingRoot() { h.missingRoot.Inc(1) } +func (h *handlerStats) IncTrieError() { h.trieError.Inc(1) } +func (h *handlerStats) IncProofError() { h.proofError.Inc(1) } +func (h *handlerStats) IncSnapshotReadError() { h.snapshotReadError.Inc(1) } +func (h *handlerStats) IncSnapshotReadAttempt() { h.snapshotReadAttempt.Inc(1) } +func (h *handlerStats) IncSnapshotReadSuccess() { h.snapshotReadSuccess.Inc(1) } +func (h *handlerStats) IncSnapshotSegmentValid() { h.snapshotSegmentValid.Inc(1) } +func (h *handlerStats) IncSnapshotSegmentInvalid() { h.snapshotSegmentInvalid.Inc(1) } + +// GetOrRegisterHandlerStats returns a [HandlerStats] to track state sync handler metrics. +// If `enabled` is false, a no-op implementation is returned. +// if `enabled` is true, calling this multiple times will return the same registered metrics. +func GetOrRegisterHandlerStats(enabled bool) HandlerStats { + if !enabled { + return NewNoopHandlerStats() + } + return &handlerStats{ + // initialize block request stats + blockRequest: metrics.GetOrRegisterCounter("block_request_count", nil), + missingBlockHash: metrics.GetOrRegisterCounter("block_request_missing_block_hash", nil), + blocksReturned: metrics.GetOrRegisterHistogram("block_request_total_blocks", nil, metrics.NewExpDecaySample(1028, 0.015)), + blockRequestProcessingTime: metrics.GetOrRegisterTimer("block_request_processing_time", nil), + + // initialize code request stats + codeRequest: metrics.GetOrRegisterCounter("code_request_count", nil), + missingCodeHash: metrics.GetOrRegisterCounter("code_request_missing_code_hash", nil), + tooManyHashesRequested: metrics.GetOrRegisterCounter("code_request_too_many_hashes", nil), + duplicateHashesRequested: metrics.GetOrRegisterCounter("code_request_duplicate_hashes", nil), + codeReadDuration: metrics.GetOrRegisterTimer("code_request_read_time", nil), + codeBytesReturned: metrics.GetOrRegisterHistogram("code_request_bytes_returned", nil, metrics.NewExpDecaySample(1028, 0.015)), + + // initialize leafs request stats + leafsRequest: metrics.GetOrRegisterCounter("leafs_request_count", nil), + invalidLeafsRequest: metrics.GetOrRegisterCounter("leafs_request_invalid", nil), + leafsRequestProcessingTime: metrics.GetOrRegisterTimer("leafs_request_processing_time", nil), + leafsReturned: metrics.GetOrRegisterHistogram("leafs_request_total_leafs", nil, metrics.NewExpDecaySample(1028, 0.015)), + leafsReadTime: metrics.GetOrRegisterTimer("leafs_request_read_time", nil), + snapshotReadTime: metrics.GetOrRegisterTimer("leafs_request_snapshot_read_time", nil), + generateRangeProofTime: metrics.GetOrRegisterTimer("leafs_request_generate_range_proof_time", nil), + proofValsReturned: metrics.GetOrRegisterHistogram("leafs_request_proof_vals_returned", nil, metrics.NewExpDecaySample(1028, 0.015)), + missingRoot: metrics.GetOrRegisterCounter("leafs_request_missing_root", nil), + trieError: metrics.GetOrRegisterCounter("leafs_request_trie_error", nil), + proofError: metrics.GetOrRegisterCounter("leafs_request_proof_error", nil), + snapshotReadError: metrics.GetOrRegisterCounter("leafs_request_snapshot_read_error", nil), + snapshotReadAttempt: metrics.GetOrRegisterCounter("leafs_request_snapshot_read_attempt", nil), + snapshotReadSuccess: metrics.GetOrRegisterCounter("leafs_request_snapshot_read_success", nil), + snapshotSegmentValid: metrics.GetOrRegisterCounter("leafs_request_snapshot_segment_valid", nil), + snapshotSegmentInvalid: metrics.GetOrRegisterCounter("leafs_request_snapshot_segment_invalid", nil), + } +} + +// no op implementation +type noopHandlerStats struct{} + +func NewNoopHandlerStats() HandlerStats { + return &noopHandlerStats{} +} + +// all operations are no-ops +func (*noopHandlerStats) IncBlockRequest() {} +func (*noopHandlerStats) IncMissingBlockHash() {} +func (*noopHandlerStats) UpdateBlocksReturned(uint16) {} +func (*noopHandlerStats) UpdateBlockRequestProcessingTime(time.Duration) {} +func (*noopHandlerStats) IncCodeRequest() {} +func (*noopHandlerStats) IncMissingCodeHash() {} +func (*noopHandlerStats) IncTooManyHashesRequested() {} +func (*noopHandlerStats) IncDuplicateHashesRequested() {} +func (*noopHandlerStats) UpdateCodeReadTime(time.Duration) {} +func (*noopHandlerStats) UpdateCodeBytesReturned(uint32) {} +func (*noopHandlerStats) IncLeafsRequest() {} +func (*noopHandlerStats) IncInvalidLeafsRequest() {} +func (*noopHandlerStats) UpdateLeafsRequestProcessingTime(time.Duration) {} +func (*noopHandlerStats) UpdateLeafsReturned(uint16) {} +func (*noopHandlerStats) UpdateReadLeafsTime(_ time.Duration) {} +func (*noopHandlerStats) UpdateSnapshotReadTime(_ time.Duration) {} +func (*noopHandlerStats) UpdateGenerateRangeProofTime(_ time.Duration) {} +func (*noopHandlerStats) UpdateRangeProofValsReturned(_ int64) {} +func (*noopHandlerStats) IncMissingRoot() {} +func (*noopHandlerStats) IncTrieError() {} +func (*noopHandlerStats) IncProofError() {} +func (*noopHandlerStats) IncSnapshotReadError() {} +func (*noopHandlerStats) IncSnapshotReadAttempt() {} +func (*noopHandlerStats) IncSnapshotReadSuccess() {} +func (*noopHandlerStats) IncSnapshotSegmentValid() {} +func (*noopHandlerStats) IncSnapshotSegmentInvalid() {} diff --git a/vms/evm/sync/stats/syncer_stats.go b/vms/evm/sync/stats/syncer_stats.go new file mode 100644 index 000000000000..6bba3344e755 --- /dev/null +++ b/vms/evm/sync/stats/syncer_stats.go @@ -0,0 +1,135 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package stats + +import ( + "fmt" + "time" + + "github.com/ava-labs/libevm/metrics" + + "github.com/ava-labs/avalanchego/vms/evm/sync/message" +) + +var ( + _ ClientSyncerStats = (*clientSyncerStats)(nil) + _ ClientSyncerStats = (*noopStats)(nil) +) + +type ClientSyncerStats interface { + GetMetric(message.Request) (MessageMetric, error) +} + +type MessageMetric interface { + IncRequested() + IncSucceeded() + IncFailed() + IncInvalidResponse() + IncReceived(int64) + UpdateRequestLatency(time.Duration) +} + +type messageMetric struct { + requested metrics.Counter // Number of times a request has been sent + succeeded metrics.Counter // Number of times a request has succeeded + failed metrics.Counter // Number of times a request failed (does not include invalid responses) + invalidResponse metrics.Counter // Number of times a request failed due to an invalid response + received metrics.Counter // Number of items that have been received + + requestLatency metrics.Timer // Latency for this request +} + +func NewMessageMetric(name string) MessageMetric { + return &messageMetric{ + requested: metrics.GetOrRegisterCounter(name+"_requested", nil), + succeeded: metrics.GetOrRegisterCounter(name+"_succeeded", nil), + failed: metrics.GetOrRegisterCounter(name+"_failed", nil), + invalidResponse: metrics.GetOrRegisterCounter(name+"_invalid_response", nil), + received: metrics.GetOrRegisterCounter(name+"_received", nil), + requestLatency: metrics.GetOrRegisterTimer(name+"_request_latency", nil), + } +} + +func (m *messageMetric) IncRequested() { + m.requested.Inc(1) +} + +func (m *messageMetric) IncSucceeded() { + m.succeeded.Inc(1) +} + +func (m *messageMetric) IncFailed() { + m.failed.Inc(1) +} + +func (m *messageMetric) IncInvalidResponse() { + m.invalidResponse.Inc(1) +} + +func (m *messageMetric) IncReceived(size int64) { + m.received.Inc(size) +} + +func (m *messageMetric) UpdateRequestLatency(duration time.Duration) { + m.requestLatency.Update(duration) +} + +type clientSyncerStats struct { + leafMetrics map[message.NodeType]MessageMetric + codeRequestMetric, + blockRequestMetric MessageMetric +} + +// NewClientSyncerStats returns stats for the client syncer +func NewClientSyncerStats(leafMetricNames map[message.NodeType]string) *clientSyncerStats { + leafMetrics := make(map[message.NodeType]MessageMetric, len(leafMetricNames)) + for nodeType, name := range leafMetricNames { + leafMetrics[nodeType] = NewMessageMetric(name) + } + return &clientSyncerStats{ + leafMetrics: leafMetrics, + codeRequestMetric: NewMessageMetric("sync_code"), + blockRequestMetric: NewMessageMetric("sync_blocks"), + } +} + +// GetMetric returns the appropriate messaage metric for the given request +func (c *clientSyncerStats) GetMetric(msgIntf message.Request) (MessageMetric, error) { + switch msg := msgIntf.(type) { + case message.BlockRequest: + return c.blockRequestMetric, nil + case message.CodeRequest: + return c.codeRequestMetric, nil + case message.LeafsRequest: + metric, ok := c.leafMetrics[msg.NodeType] + if !ok { + return nil, fmt.Errorf("invalid leafs request for node type: %T", msg.NodeType) + } + return metric, nil + default: + return nil, fmt.Errorf("attempted to get metric for invalid request with type %T", msg) + } +} + +// no-op implementation of ClientSyncerStats +type noopStats struct { + noop noopMsgMetric +} + +type noopMsgMetric struct{} + +func (noopMsgMetric) IncRequested() {} +func (noopMsgMetric) IncSucceeded() {} +func (noopMsgMetric) IncFailed() {} +func (noopMsgMetric) IncInvalidResponse() {} +func (noopMsgMetric) IncReceived(int64) {} +func (noopMsgMetric) UpdateRequestLatency(time.Duration) {} + +func NewNoOpStats() ClientSyncerStats { + return &noopStats{} +} + +func (n noopStats) GetMetric(_ message.Request) (MessageMetric, error) { + return n.noop, nil +}