diff --git a/internal/consensus/metrics.go b/internal/consensus/metrics.go index a75f1505c..c33f17a0d 100644 --- a/internal/consensus/metrics.go +++ b/internal/consensus/metrics.go @@ -64,6 +64,21 @@ type Metrics struct { // Histogram of time taken per step annotated with reason that the step proceeded. StepTime metrics.Histogram + + // QuroumPrevoteMessageDelay is the difference in seconds between the proposal + // timestamp and the timestamp of the prevote that achieved a quorum in the prevote step. + // The prevote that achieved a quorum in the prevote step is determined by first + // sorting all of the collected prevotes in a round in ascending order of + // timestamp and iterating over this list from lowest timestamp to highest. + // During iteration, the total represented by iterated prevotes is kept track of. + // The prevote that achieved a quorum is the prevote that brings the summed + // voting power in this iteration to above 2/3 of the power on the network. + QuorumPrevoteMessageDelay metrics.Gauge + + // FullPrevoteMessageDelay is the difference in seconds between the proposal + // timestamp and the timestamp of the prevote that achieved 100% of the voting + // power on the network in the prevote step when prevotes are ordered by timestamp. + FullPrevoteMessageDelay metrics.Gauge } // PrometheusMetrics returns Metrics build using Prometheus client library. @@ -196,6 +211,20 @@ func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { Name: "step_time", Help: "Time spent per step.", }, append(labels, "step", "reason")).With(labelsAndValues...), + QuorumPrevoteMessageDelay: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "quorum_prevote_message_delay", + Help: "Difference in seconds between the proposal timestamp and the timestamp " + + "of the prevote that achieved a quorum in the prevote step.", + }, labels).With(labelsAndValues...), + FullPrevoteMessageDelay: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "complete_prevote_message_delay", + Help: "Difference in seconds between the proposal timestamp and the timestamp " + + "of the prevote that achieved 100% of the voting power in the prevote step.", + }, labels).With(labelsAndValues...), } } diff --git a/internal/consensus/state.go b/internal/consensus/state.go index 363a4beae..dc55a6a87 100644 --- a/internal/consensus/state.go +++ b/internal/consensus/state.go @@ -8,6 +8,7 @@ import ( "io" "os" "runtime/debug" + "sort" "sync" "time" @@ -1680,6 +1681,8 @@ func (cs *State) finalizeCommit(ctx context.Context, height int64) { return } + cs.calculatePrevoteMessageDelayMetrics() + blockID, ok := cs.Votes.Precommits(cs.CommitRound).TwoThirdsMajority() block, blockParts := cs.ProposalBlock, cs.ProposalBlockParts @@ -2414,6 +2417,26 @@ func (cs *State) checkDoubleSigningRisk(height int64) error { return nil } +func (cs *State) calculatePrevoteMessageDelayMetrics() { + ps := cs.Votes.Prevotes(cs.Round) + pl := ps.List() + sort.Slice(pl, func(i, j int) bool { + return pl[i].Timestamp.Before(pl[j].Timestamp) + }) + var votingPowerSeen int64 + for _, v := range pl { + _, val := cs.Validators.GetByAddress(v.ValidatorAddress) + votingPowerSeen += val.VotingPower + if votingPowerSeen >= cs.Validators.TotalVotingPower()*(2/3+1) { + cs.metrics.QuorumPrevoteMessageDelay.Set(v.Timestamp.Sub(cs.Proposal.Timestamp).Seconds()) + break + } + } + if ps.HasAll() { + cs.metrics.FullPrevoteMessageDelay.Set(pl[len(pl)-1].Timestamp.Sub(cs.Proposal.Timestamp).Seconds()) + } +} + //--------------------------------------------------------- func CompareHRS(h1 int64, r1 int32, s1 cstypes.RoundStepType, h2 int64, r2 int32, s2 cstypes.RoundStepType) int { diff --git a/types/vote_set.go b/types/vote_set.go index 46e6d270d..5c29e3d34 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -378,6 +378,15 @@ func (voteSet *VoteSet) GetByIndex(valIndex int32) *Vote { return voteSet.votes[valIndex] } +// List returns a copy of the list of votes stored by the VoteSet. +func (voteSet *VoteSet) List() []Vote { + votes := make([]Vote, len(voteSet.votes)) + for i := range voteSet.votes { + votes[i] = *voteSet.votes[i] + } + return votes +} + func (voteSet *VoteSet) GetByAddress(address []byte) *Vote { if voteSet == nil { return nil