diff --git a/chain/consensus/compute_state.go b/chain/consensus/compute_state.go index 66257ded083..5e92d17a5dd 100644 --- a/chain/consensus/compute_state.go +++ b/chain/consensus/compute_state.go @@ -2,6 +2,7 @@ package consensus import ( "context" + "sort" "sync/atomic" "time" @@ -215,6 +216,18 @@ func (t *TipSetExecutor) ApplyBlocks(ctx context.Context, var msgGas int64 + // Track gas metrics for synced blocks + type msgGasMetrics struct { + gasLimit int64 + gasUsed int64 + } + var ( + totalGasLimit int64 + totalGasUsed int64 + messageCount int64 + msgGasList []msgGasMetrics + ) + for _, b := range bms { penalty := types.NewInt(0) gasReward := big.Zero() @@ -231,6 +244,16 @@ func (t *TipSetExecutor) ApplyBlocks(ctx context.Context, msgGas += r.GasUsed + // Accumulate gas metrics + totalGasLimit += m.GasLimit + totalGasUsed += r.GasUsed + messageCount++ + msgGasList = append(msgGasList, msgGasMetrics{gasLimit: m.GasLimit, gasUsed: r.GasUsed}) + + // Record per-message histograms + stats.Record(ctx, metrics.SyncedBlockMessageGasLimit.M(m.GasLimit)) + stats.Record(ctx, metrics.SyncedBlockMessageGasUsed.M(r.GasUsed)) + receipts = append(receipts, &r.MessageReceipt) gasReward = big.Add(gasReward, r.GasCosts.MinerTip) penalty = big.Add(penalty, r.GasCosts.MinerPenalty) @@ -330,6 +353,45 @@ func (t *TipSetExecutor) ApplyBlocks(ctx context.Context, metrics.VMApplyCronGas.M(cronGas), ) + // Record synced block gas metrics + stats.Record(ctx, + metrics.SyncedBlockTotalGasLimit.M(totalGasLimit), + metrics.SyncedBlockTotalGasUsed.M(totalGasUsed), + metrics.SyncedBlockMessageCount.M(messageCount), + ) + + // Compute and record medians if there are messages + if len(msgGasList) > 0 { + // Median gas limit (by message count) + sort.Slice(msgGasList, func(i, j int) bool { return msgGasList[i].gasLimit < msgGasList[j].gasLimit }) + stats.Record(ctx, metrics.SyncedBlockGasLimitMedian.M(msgGasList[len(msgGasList)/2].gasLimit)) + + // Gas-weighted median of gas limit (what gas limit the median executed gas unit had) + halfGasUsed := totalGasUsed / 2 + var accumulatedGas int64 + for _, m := range msgGasList { + accumulatedGas += m.gasUsed + if accumulatedGas >= halfGasUsed { + stats.Record(ctx, metrics.SyncedBlockGasLimitMedianByGasUnits.M(m.gasLimit)) + break + } + } + + // Median gas used (by message count) + sort.Slice(msgGasList, func(i, j int) bool { return msgGasList[i].gasUsed < msgGasList[j].gasUsed }) + stats.Record(ctx, metrics.SyncedBlockGasUsedMedian.M(msgGasList[len(msgGasList)/2].gasUsed)) + + // Gas-weighted median of gas used (what gas used the median gas unit had) + accumulatedGas = 0 + for _, m := range msgGasList { + accumulatedGas += m.gasUsed + if accumulatedGas >= halfGasUsed { + stats.Record(ctx, metrics.SyncedBlockGasUsedMedianByGasUnits.M(m.gasUsed)) + break + } + } + } + return st, rectroot, nil } diff --git a/chain/messagepool/messagepool.go b/chain/messagepool/messagepool.go index de5ca83fcb1..5339ce404be 100644 --- a/chain/messagepool/messagepool.go +++ b/chain/messagepool/messagepool.go @@ -1440,6 +1440,11 @@ func (mp *MessagePool) HeadChange(ctx context.Context, revert []*types.TipSet, a } } + // Record gas metrics after head change + mp.lk.RLock() + mp.recordGasMetrics(ctx) + mp.lk.RUnlock() + return merr } @@ -1666,6 +1671,106 @@ func getBaseFeeLowerBound(baseFee, factor types.BigInt) types.BigInt { return baseFeeLowerBound } +// msgGasInfo holds gas-related info for a message, used for computing weighted medians +type msgGasInfo struct { + gasLimit int64 + gasPremium big.Int + gasFeeCap big.Int +} + +// bigIntToFloat64 converts a big.Int to float64 for metrics recording +func bigIntToFloat64(b big.Int) float64 { + if b.Int == nil { + return 0 + } + f, _ := new(stdbig.Float).SetInt(b.Int).Float64() + return f +} + +// recordGasMetrics records gas-related metrics for the current mpool state. +// Should be called with mp.lk held (at least RLock) and mp.curTsLk held. +func (mp *MessagePool) recordGasMetrics(ctx context.Context) { + // Get basefee from current tipset + if mp.curTs != nil && len(mp.curTs.Blocks()) > 0 { + baseFee := mp.curTs.Blocks()[0].ParentBaseFee + stats.Record(ctx, metrics.BaseFee.M(bigIntToFloat64(baseFee))) + } + + // Collect gas metrics from pending messages + var totalGasLimit int64 + var totalPremium big.Int = big.Zero() + var totalFeeCap big.Int = big.Zero() + var msgs []msgGasInfo + + mp.forEachPending(func(_ address.Address, s *msgSet) { + for _, m := range s.msgs { + gasLimit := m.Message.GasLimit + totalGasLimit += gasLimit + totalPremium = big.Add(totalPremium, m.Message.GasPremium) + totalFeeCap = big.Add(totalFeeCap, m.Message.GasFeeCap) + msgs = append(msgs, msgGasInfo{ + gasLimit: gasLimit, + gasPremium: m.Message.GasPremium, + gasFeeCap: m.Message.GasFeeCap, + }) + // Record histograms + stats.Record(ctx, metrics.MpoolMessageGasLimit.M(gasLimit)) + stats.Record(ctx, metrics.MpoolGasPremium.M(bigIntToFloat64(m.Message.GasPremium))) + stats.Record(ctx, metrics.MpoolGasFeeCap.M(bigIntToFloat64(m.Message.GasFeeCap))) + } + }) + + msgCount := int64(len(msgs)) + stats.Record(ctx, metrics.MpoolTotalGasLimit.M(totalGasLimit)) + + // Record mean and median gas metrics (avoid division by zero) + if msgCount > 0 { + // Gas Premium statistics + meanPremium := big.Div(totalPremium, big.NewInt(msgCount)) + stats.Record(ctx, metrics.MpoolGasPremiumMean.M(bigIntToFloat64(meanPremium))) + + // Gas Fee Cap statistics + meanFeeCap := big.Div(totalFeeCap, big.NewInt(msgCount)) + stats.Record(ctx, metrics.MpoolGasFeeCapMean.M(bigIntToFloat64(meanFeeCap))) + + // Compute median gas limit (by message count) + sort.Slice(msgs, func(i, j int) bool { return msgs[i].gasLimit < msgs[j].gasLimit }) + medianGasLimit := msgs[len(msgs)/2].gasLimit + stats.Record(ctx, metrics.MpoolGasLimitMedian.M(medianGasLimit)) + + // Compute median gas premium (by message count) + sort.Slice(msgs, func(i, j int) bool { return msgs[i].gasPremium.LessThan(msgs[j].gasPremium) }) + medianPremium := msgs[len(msgs)/2].gasPremium + stats.Record(ctx, metrics.MpoolGasPremiumMedian.M(bigIntToFloat64(medianPremium))) + + // Compute gas-weighted median of gas premium + halfGas := totalGasLimit / 2 + var accumulatedGas int64 + for _, m := range msgs { + accumulatedGas += m.gasLimit + if accumulatedGas >= halfGas { + stats.Record(ctx, metrics.MpoolGasPremiumMedianByGasUnits.M(bigIntToFloat64(m.gasPremium))) + break + } + } + + // Compute median gas fee cap (by message count) + sort.Slice(msgs, func(i, j int) bool { return msgs[i].gasFeeCap.LessThan(msgs[j].gasFeeCap) }) + medianFeeCap := msgs[len(msgs)/2].gasFeeCap + stats.Record(ctx, metrics.MpoolGasFeeCapMedian.M(bigIntToFloat64(medianFeeCap))) + + // Compute gas-weighted median of gas fee cap + accumulatedGas = 0 + for _, m := range msgs { + accumulatedGas += m.gasLimit + if accumulatedGas >= halfGas { + stats.Record(ctx, metrics.MpoolGasFeeCapMedianByGasUnits.M(bigIntToFloat64(m.gasFeeCap))) + break + } + } + } +} + type MpoolNonceAPI interface { GetNonce(context.Context, address.Address, types.TipSetKey) (uint64, error) GetActor(context.Context, address.Address, types.TipSetKey) (*types.Actor, error) diff --git a/metrics/metrics.go b/metrics/metrics.go index 940e547a45c..f55bb8f1f60 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -34,7 +34,23 @@ var workMillisecondsDistribution = view.Distribution( var queueSizeDistribution = view.Distribution(0, 1, 2, 3, 5, 7, 10, 15, 25, 35, 50, 70, 90, 130, 200, 300, 500, 1000, 2000, 5000, 10000) -// Global Tags +// Gas distribution for message gas limits and used gas (ranges from thousands to billions) +var gasDistribution = view.Distribution( + 250e3, 500e3, // very small messages + 1e6, 2e6, 5e6, 10e6, 20e6, 50e6, 100e6, // typical messages + 200e6, 500e6, 1e9, 2e9, 5e9, 10e9, // large messages up to block limit +) + +// Gas fee distribution for gas premium and gas fee cap (100 attoFIL to 10 nanoFIL) +// 10 nanoFIL = 10 * 1e9 attoFIL = 1e10 attoFIL +var gasFeeDistribution = view.Distribution( + 100, 200, 500, // very low fees (100-500 attoFIL) + 1e3, 500e3, // femtoFIL range (1e3 = 1 femtoFIL) + 1e6, 100e6, 500e6, // picoFIL range (1e6 = 1 picoFIL) + 1e9, 5e9, 10e9, 50e9, // nanoFIL range (1e9 = 1 nanoFIL) +) + +// Tags var ( // common Version, _ = tag.NewKey("version") @@ -141,6 +157,30 @@ var ( MessageFetchNetwork = stats.Int64("message/fetch_network", "Number of messages fetched from network", stats.UnitDimensionless) MessageFetchDuration = stats.Float64("message/fetch_duration_ms", "Duration of message fetch operations", stats.UnitMilliseconds) + // gas metrics + // Note: attoFIL metrics use Float64 because values can exceed int64 range (atto = 1e-18) + BaseFee = stats.Float64("chain/basefee", "Current base fee in attoFIL", stats.UnitDimensionless) + MpoolGasPremium = stats.Float64("mpool/gas_premium", "Gas premium of messages in mpool in attoFIL (histogram)", stats.UnitDimensionless) + MpoolGasPremiumMean = stats.Float64("mpool/gas_premium_mean", "Mean gas premium of messages in mpool in attoFIL", stats.UnitDimensionless) + MpoolGasPremiumMedian = stats.Float64("mpool/gas_premium_median", "Median gas premium of messages in mpool in attoFIL", stats.UnitDimensionless) + MpoolGasPremiumMedianByGasUnits = stats.Float64("mpool/gas_premium_median_by_gas_units", "Median gas premium weighted by gas limit (what premium the median gas unit pays)", stats.UnitDimensionless) + MpoolGasFeeCap = stats.Float64("mpool/gas_fee_cap", "Gas fee cap of messages in mpool in attoFIL (histogram)", stats.UnitDimensionless) + MpoolGasFeeCapMean = stats.Float64("mpool/gas_fee_cap_mean", "Mean gas fee cap of messages in mpool in attoFIL", stats.UnitDimensionless) + MpoolGasFeeCapMedian = stats.Float64("mpool/gas_fee_cap_median", "Median gas fee cap of messages in mpool in attoFIL", stats.UnitDimensionless) + MpoolGasFeeCapMedianByGasUnits = stats.Float64("mpool/gas_fee_cap_median_by_gas_units", "Median gas fee cap weighted by gas limit", stats.UnitDimensionless) + MpoolTotalGasLimit = stats.Int64("mpool/total_gas_limit", "Total gas limit of all messages in mpool", stats.UnitDimensionless) + MpoolMessageGasLimit = stats.Int64("mpool/message_gas_limit", "Gas limit of messages in mpool (histogram)", stats.UnitDimensionless) + MpoolGasLimitMedian = stats.Int64("mpool/gas_limit_median", "Median gas limit of messages in mpool", stats.UnitDimensionless) + SyncedBlockTotalGasLimit = stats.Int64("chain/synced_block_total_gas_limit", "Total gas limit of all messages in synced block", stats.UnitDimensionless) + SyncedBlockMessageGasLimit = stats.Int64("chain/synced_block_message_gas_limit", "Gas limit of messages in synced blocks (histogram)", stats.UnitDimensionless) + SyncedBlockGasLimitMedian = stats.Int64("chain/synced_block_gas_limit_median", "Median gas limit of messages in synced block", stats.UnitDimensionless) + SyncedBlockGasLimitMedianByGasUnits = stats.Int64("chain/synced_block_gas_limit_median_by_gas_units", "Median gas limit weighted by gas used", stats.UnitDimensionless) + SyncedBlockTotalGasUsed = stats.Int64("chain/synced_block_total_gas_used", "Total actual gas used in synced block", stats.UnitDimensionless) + SyncedBlockMessageGasUsed = stats.Int64("chain/synced_block_message_gas_used", "Actual gas used per message in synced blocks (histogram)", stats.UnitDimensionless) + SyncedBlockGasUsedMedian = stats.Int64("chain/synced_block_gas_used_median", "Median gas used per message in synced block", stats.UnitDimensionless) + SyncedBlockGasUsedMedianByGasUnits = stats.Int64("chain/synced_block_gas_used_median_by_gas_units", "Median gas used weighted by gas used", stats.UnitDimensionless) + SyncedBlockMessageCount = stats.Int64("chain/synced_block_message_count", "Number of messages in synced block", stats.UnitDimensionless) + // miner WorkerCallsStarted = stats.Int64("sealing/worker_calls_started", "Counter of started worker tasks", stats.UnitDimensionless) WorkerCallsReturnedCount = stats.Int64("sealing/worker_calls_returned_count", "Counter of returned worker tasks", stats.UnitDimensionless) @@ -484,6 +524,113 @@ var ( TagKeys: []tag.Key{FetchSource, Network}, } + // gas metrics + BaseFeeView = &view.View{ + Measure: BaseFee, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + MpoolGasPremiumView = &view.View{ + Measure: MpoolGasPremium, + Aggregation: gasFeeDistribution, + TagKeys: []tag.Key{Network}, + } + MpoolGasPremiumMeanView = &view.View{ + Measure: MpoolGasPremiumMean, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + MpoolGasPremiumMedianView = &view.View{ + Measure: MpoolGasPremiumMedian, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + MpoolGasPremiumMedianByGasUnitsView = &view.View{ + Measure: MpoolGasPremiumMedianByGasUnits, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + MpoolGasFeeCapView = &view.View{ + Measure: MpoolGasFeeCap, + Aggregation: gasFeeDistribution, + TagKeys: []tag.Key{Network}, + } + MpoolGasFeeCapMeanView = &view.View{ + Measure: MpoolGasFeeCapMean, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + MpoolGasFeeCapMedianView = &view.View{ + Measure: MpoolGasFeeCapMedian, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + MpoolGasFeeCapMedianByGasUnitsView = &view.View{ + Measure: MpoolGasFeeCapMedianByGasUnits, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + MpoolTotalGasLimitView = &view.View{ + Measure: MpoolTotalGasLimit, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + MpoolMessageGasLimitView = &view.View{ + Measure: MpoolMessageGasLimit, + Aggregation: gasDistribution, + TagKeys: []tag.Key{Network}, + } + MpoolGasLimitMedianView = &view.View{ + Measure: MpoolGasLimitMedian, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + SyncedBlockTotalGasLimitView = &view.View{ + Measure: SyncedBlockTotalGasLimit, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + SyncedBlockMessageGasLimitView = &view.View{ + Measure: SyncedBlockMessageGasLimit, + Aggregation: gasDistribution, + TagKeys: []tag.Key{Network}, + } + SyncedBlockGasLimitMedianView = &view.View{ + Measure: SyncedBlockGasLimitMedian, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + SyncedBlockGasLimitMedianByGasUnitsView = &view.View{ + Measure: SyncedBlockGasLimitMedianByGasUnits, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + SyncedBlockTotalGasUsedView = &view.View{ + Measure: SyncedBlockTotalGasUsed, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + SyncedBlockMessageGasUsedView = &view.View{ + Measure: SyncedBlockMessageGasUsed, + Aggregation: gasDistribution, + TagKeys: []tag.Key{Network}, + } + SyncedBlockGasUsedMedianView = &view.View{ + Measure: SyncedBlockGasUsedMedian, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + SyncedBlockGasUsedMedianByGasUnitsView = &view.View{ + Measure: SyncedBlockGasUsedMedianByGasUnits, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + SyncedBlockMessageCountView = &view.View{ + Measure: SyncedBlockMessageCount, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{Network}, + } + // miner WorkerCallsStartedView = &view.View{ Measure: WorkerCallsStarted, @@ -864,6 +1011,27 @@ var ChainNodeViews = append([]*view.View{ MessageFetchLocalView, MessageFetchNetworkView, MessageFetchDurationView, + BaseFeeView, + MpoolGasPremiumView, + MpoolGasPremiumMeanView, + MpoolGasPremiumMedianView, + MpoolGasPremiumMedianByGasUnitsView, + MpoolGasFeeCapView, + MpoolGasFeeCapMeanView, + MpoolGasFeeCapMedianView, + MpoolGasFeeCapMedianByGasUnitsView, + MpoolTotalGasLimitView, + MpoolMessageGasLimitView, + MpoolGasLimitMedianView, + SyncedBlockTotalGasLimitView, + SyncedBlockMessageGasLimitView, + SyncedBlockGasLimitMedianView, + SyncedBlockGasLimitMedianByGasUnitsView, + SyncedBlockTotalGasUsedView, + SyncedBlockMessageGasUsedView, + SyncedBlockGasUsedMedianView, + SyncedBlockGasUsedMedianByGasUnitsView, + SyncedBlockMessageCountView, }, DefaultViews...) var MinerNodeViews = append([]*view.View{