Skip to content

feat(gasmeter): support detailed metering and reporting#4953

Open
aeddi wants to merge 17 commits intognolang:masterfrom
aeddi:gas-details
Open

feat(gasmeter): support detailed metering and reporting#4953
aeddi wants to merge 17 commits intognolang:masterfrom
aeddi:gas-details

Conversation

@aeddi
Copy link
Contributor

@aeddi aeddi commented Dec 9, 2025

Summary

This PR refactors the gas metering system to make its usage more consistent across the codebase, supporting detailed metering and reporting.

Changes

1. Move gas meter to dedicated package (a9f9eeb)

This commit moves the gas meter out of the tm2/pkg/store package to its own package under tm2/pkg/gas, since it's not only related to store operations anymore. It also renames some functions to be more aligned with Go best practices (e.g. gas.NewGasMetergas.NewMeter).

2. Centralize gas operations and costs (00cb42e)

This commit makes gas metering usage more consistent. Previously, the metering was scattered across the codebase:

  • Store: descriptors and costs located in gas package
  • Opcode: costs and only one literal descriptor for all opcodes located in gnovm/pkg/gnolang/machine.go, using a multiplier in gnovm/pkg/gnolang/machine.go
  • Alloc: literal descriptor and cost located in gnovm/pkg/gnolang/alloc.go
  • Garbage: literal descriptor and cost located in gnovm/pkg/gnolang/garbage_collector.go, using a multiplier in gnovm/pkg/gnolang/machine.go
  • Parsing: literal descriptor and costs located in gnovm/pkg/gnolang/go2gno.go
  • KVStore: literal descriptor and default costs located in gnovm/pkg/gnolang/store.go, costs modifiable by param of the store
  • Native print: literal descriptor and cost located in gnovm/pkg/gnolang/uverse.go
  • Transaction: literal descriptor and costs located in tm2/pkg/sdk/auth/ante.go, costs can be changed by params in tm2/pkg/sdk/auth/params.go

After this commit:

  • Everything is centralized inside the gas package with operations in tm2/pkg/gas/operation.go and the associated costs in tm2/pkg/gas/config.go.
  • The cost config (including the global multiplier) is passed at gas meter creation and the API requires specifying an operation ID + a multiplier. So:
    • If a cost is flat (like for a CPU opcode operation), the usage is gasMeter.GasConsume(OpCPUCall, 1).
    • If a cost is per unit (like printing bytes), the usage is gasMeter.GasConsume(OpNativePrintPerByte, len(output)).
  • Cost and multiplier are both float64 to allow division through this simple API (multiply by 0.5 to get half, etc.), but the gas counting still uses int64.

3. Add float support to overflow package (ebdf7a1)

This commit adds support for float operations to the overflow package, since the gas metering now uses float64 for cost and multiplier.

4. Add detailed gas reporting (dd954ff)

This commit adds detailed gas report support. The gas meter now keeps track of how many times an operation was performed and how much gas it consumed in total. Operations are grouped into categories so we can produce reports per category.

Note: Category mapping is done on access to keep gas metering as fast as possible by relying on a simple fixed-size array to store the counters.

5. Deduplicate tx info printing (07b6cf2)

This commit deduplicates the tx info printing that was duplicated in 3 different places. Now this part only calls the PrintTxInfo function from info.go.

6. Add verbose flag to gnokey (92b7b86)

This commit adds a new verbosity flag to gnokey to display 4 levels of gas reports.

-v 0     verbosity level for gas detail:
            - 0: no detail (default)
            - 1: gas by category
            - 2: gas by category + operations (excluding zero count)
            - 3: gas by category + all operations (including zero count)

Note: The default keep the current behavior so we don't break any existing tooling relying on gnokey output.

Examples

Level 0 output
$> gnokey maketx call -pkgpath "gno.land/r/demo/counter" -func "Increment" -gas-fee 10000000ugnot -gas-wanted 8000000 -broadcast test1 -v 0
Enter password.
(13 int)


OK!
GAS WANTED: 8000000
GAS USED:   134680
HEIGHT:     26
EVENTS:     []
INFO:
TX HASH:    oaxNsfsYJHOqIoZS0naoac6VRfmch7jBU9JvOKj5fsI=
Level 1 output
$> gnokey maketx call -pkgpath "gno.land/r/demo/counter" -func "Increment" -gas-fee 10000000ugnot -gas-wanted 8000000 -broadcast test1 -v 1
Enter password.
(14 int)


OK!
GAS WANTED: 8000000
GAS USED:           Operation: 73    Gas: 134680
├── CPU:            Operation: 14    Gas: 941
├── KVStore:        Operation: 8     Gas: 88384
├── Memory:         Operation: 5     Gas: 1192
├── Parsing:        Operation: 14    Gas: 23
├── Store:          Operation: 30    Gas: 40700
└── Transaction:    Operation: 2     Gas: 3440

HEIGHT:     28
EVENTS:     []
INFO:
TX HASH:    +oOuQjoLU67TDW8uwAkgzM/OM958bY/1eBdpF8ln66o=
Level 2 output
$> gnokey maketx call -pkgpath "gno.land/r/demo/counter" -func "Increment" -gas-fee 10000000ugnot -gas-wanted 8000000 -broadcast test1 -v 2
Enter password.
(15 int)


OK!
GAS WANTED: 8000000
GAS USED:                                 Operation: 73    Gas: 134680
├── CPU:                                  Operation: 14    Gas: 941
│   ├── CPUHalt:                          Operation: 1     Gas: 1
│   ├── CPUPrecall:                       Operation: 1     Gas: 207
│   ├── CPUEnterCrossing:                 Operation: 1     Gas: 100
│   ├── CPUCall:                          Operation: 1     Gas: 256
│   ├── CPUReturn:                        Operation: 1     Gas: 38
│   ├── CPUEval:                          Operation: 5     Gas: 145
│   ├── CPUSelector:                      Operation: 1     Gas: 32
│   ├── CPUInc:                           Operation: 1     Gas: 76
│   └── CPUBody:                          Operation: 2     Gas: 86
├── KVStore:                              Operation: 8     Gas: 88384
│   ├── KVStoreGetObjectPerByte:          Operation: 6     Gas: 45344
│   ├── KVStoreSetObjectPerByte:          Operation: 1     Gas: 3216
│   └── KVStoreGetPackageRealmPerByte:    Operation: 1     Gas: 39824
├── Memory:                               Operation: 5     Gas: 1192
│   └── MemoryAllocPerByte:               Operation: 5     Gas: 1192
├── Parsing:                              Operation: 14    Gas: 23
│   ├── ParsingToken:                     Operation: 8     Gas: 8
│   └── ParsingNesting:                   Operation: 6     Gas: 15
├── Store:                                Operation: 30    Gas: 40700
│   ├── StoreReadFlat:                    Operation: 10    Gas: 10000
│   ├── StoreReadPerByte:                 Operation: 10    Gas: 3450
│   ├── StoreWriteFlat:                   Operation: 5     Gas: 10000
│   └── StoreWritePerByte:                Operation: 5     Gas: 17250
└── Transaction:                          Operation: 2     Gas: 3440
    ├── TransactionPerByte:               Operation: 1     Gas: 2440
    └── TxansactionSigVerifySecp256k1:    Operation: 1     Gas: 1000

HEIGHT:     30
EVENTS:     []
INFO:
TX HASH:    mZpwriqYnXSPYBYUhsu6CrUI4NQovvIiwZ1vvvp2HnU=
Level 3 output
$> gnokey maketx call -pkgpath "gno.land/r/demo/counter" -func "Increment" -gas-fee 10000000ugnot -gas-wanted 8000000 -broadcast test1 -v 3
Enter password.
(16 int)


OK!
GAS WANTED: 8000000
GAS USED:                                 Operation: 73    Gas: 134680
├── BlockGas:                             Operation: 0     Gas: 0
│   └── BlockGasSum:                      Operation: 0     Gas: 0
├── CPU:                                  Operation: 14    Gas: 941
│   ├── CPUInvalid:                       Operation: 0     Gas: 0
│   ├── CPUHalt:                          Operation: 1     Gas: 1
│   ├── CPUNoop:                          Operation: 0     Gas: 0
│   ├── CPUExec:                          Operation: 0     Gas: 0
│   ├── CPUPrecall:                       Operation: 1     Gas: 207
│   ├── CPUEnterCrossing:                 Operation: 1     Gas: 100
│   ├── CPUCall:                          Operation: 1     Gas: 256
│   ├── CPUCallNativeBody:                Operation: 0     Gas: 0
│   ├── CPUDefer:                         Operation: 0     Gas: 0
│   ├── CPUCallDeferNativeBody:           Operation: 0     Gas: 0
│   ├── CPUGo:                            Operation: 0     Gas: 0
│   ├── CPUSelect:                        Operation: 0     Gas: 0
│   ├── CPUSwitchClause:                  Operation: 0     Gas: 0
│   ├── CPUSwitchClauseCase:              Operation: 0     Gas: 0
│   ├── CPUTypeSwitch:                    Operation: 0     Gas: 0
│   ├── CPUIfCond:                        Operation: 0     Gas: 0
│   ├── CPUPopValue:                      Operation: 0     Gas: 0
│   ├── CPUPopResults:                    Operation: 0     Gas: 0
│   ├── CPUPopBlock:                      Operation: 0     Gas: 0
│   ├── CPUPopFrameAndReset:              Operation: 0     Gas: 0
│   ├── CPUPanic1:                        Operation: 0     Gas: 0
│   ├── CPUPanic2:                        Operation: 0     Gas: 0
│   ├── CPUReturn:                        Operation: 1     Gas: 38
│   ├── CPUReturnAfterCopy:               Operation: 0     Gas: 0
│   ├── CPUReturnFromBlock:               Operation: 0     Gas: 0
│   ├── CPUReturnToBlock:                 Operation: 0     Gas: 0
│   ├── CPUUpos:                          Operation: 0     Gas: 0
│   ├── CPUUneg:                          Operation: 0     Gas: 0
│   ├── CPUUnot:                          Operation: 0     Gas: 0
│   ├── CPUUxor:                          Operation: 0     Gas: 0
│   ├── CPUUrecv:                         Operation: 0     Gas: 0
│   ├── CPULor:                           Operation: 0     Gas: 0
│   ├── CPULand:                          Operation: 0     Gas: 0
│   ├── CPUEql:                           Operation: 0     Gas: 0
│   ├── CPUNeq:                           Operation: 0     Gas: 0
│   ├── CPULss:                           Operation: 0     Gas: 0
│   ├── CPULeq:                           Operation: 0     Gas: 0
│   ├── CPUGtr:                           Operation: 0     Gas: 0
│   ├── CPUGeq:                           Operation: 0     Gas: 0
│   ├── CPUAdd:                           Operation: 0     Gas: 0
│   ├── CPUSub:                           Operation: 0     Gas: 0
│   ├── CPUBor:                           Operation: 0     Gas: 0
│   ├── CPUXor:                           Operation: 0     Gas: 0
│   ├── CPUMul:                           Operation: 0     Gas: 0
│   ├── CPUQuo:                           Operation: 0     Gas: 0
│   ├── CPURem:                           Operation: 0     Gas: 0
│   ├── CPUShl:                           Operation: 0     Gas: 0
│   ├── CPUShr:                           Operation: 0     Gas: 0
│   ├── CPUBand:                          Operation: 0     Gas: 0
│   ├── CPUBandn:                         Operation: 0     Gas: 0
│   ├── CPUEval:                          Operation: 5     Gas: 145
│   ├── CPUBinary1:                       Operation: 0     Gas: 0
│   ├── CPUIndex1:                        Operation: 0     Gas: 0
│   ├── CPUIndex2:                        Operation: 0     Gas: 0
│   ├── CPUSelector:                      Operation: 1     Gas: 32
│   ├── CPUSlice:                         Operation: 0     Gas: 0
│   ├── CPUStar:                          Operation: 0     Gas: 0
│   ├── CPURef:                           Operation: 0     Gas: 0
│   ├── CPUTypeAssert1:                   Operation: 0     Gas: 0
│   ├── CPUTypeAssert2:                   Operation: 0     Gas: 0
│   ├── CPUStaticTypeOf:                  Operation: 0     Gas: 0
│   ├── CPUCompositeLit:                  Operation: 0     Gas: 0
│   ├── CPUArrayLit:                      Operation: 0     Gas: 0
│   ├── CPUSliceLit:                      Operation: 0     Gas: 0
│   ├── CPUSliceLit2:                     Operation: 0     Gas: 0
│   ├── CPUMapLit:                        Operation: 0     Gas: 0
│   ├── CPUStructLit:                     Operation: 0     Gas: 0
│   ├── CPUFuncLit:                       Operation: 0     Gas: 0
│   ├── CPUConvert:                       Operation: 0     Gas: 0
│   ├── CPUFieldType:                     Operation: 0     Gas: 0
│   ├── CPUArrayType:                     Operation: 0     Gas: 0
│   ├── CPUSliceType:                     Operation: 0     Gas: 0
│   ├── CPUPointerType:                   Operation: 0     Gas: 0
│   ├── CPUInterfaceType:                 Operation: 0     Gas: 0
│   ├── CPUChanType:                      Operation: 0     Gas: 0
│   ├── CPUFuncType:                      Operation: 0     Gas: 0
│   ├── CPUMapType:                       Operation: 0     Gas: 0
│   ├── CPUStructType:                    Operation: 0     Gas: 0
│   ├── CPUAssign:                        Operation: 0     Gas: 0
│   ├── CPUAddAssign:                     Operation: 0     Gas: 0
│   ├── CPUSubAssign:                     Operation: 0     Gas: 0
│   ├── CPUMulAssign:                     Operation: 0     Gas: 0
│   ├── CPUQuoAssign:                     Operation: 0     Gas: 0
│   ├── CPURemAssign:                     Operation: 0     Gas: 0
│   ├── CPUBandAssign:                    Operation: 0     Gas: 0
│   ├── CPUBandnAssign:                   Operation: 0     Gas: 0
│   ├── CPUBorAssign:                     Operation: 0     Gas: 0
│   ├── CPUXorAssign:                     Operation: 0     Gas: 0
│   ├── CPUShlAssign:                     Operation: 0     Gas: 0
│   ├── CPUShrAssign:                     Operation: 0     Gas: 0
│   ├── CPUDefine:                        Operation: 0     Gas: 0
│   ├── CPUInc:                           Operation: 1     Gas: 76
│   ├── CPUDec:                           Operation: 0     Gas: 0
│   ├── CPUValueDecl:                     Operation: 0     Gas: 0
│   ├── CPUTypeDecl:                      Operation: 0     Gas: 0
│   ├── CPUSticky:                        Operation: 0     Gas: 0
│   ├── CPUBody:                          Operation: 2     Gas: 86
│   ├── CPUForLoop:                       Operation: 0     Gas: 0
│   ├── CPURangeIter:                     Operation: 0     Gas: 0
│   ├── CPURangeIterString:               Operation: 0     Gas: 0
│   ├── CPURangeIterMap:                  Operation: 0     Gas: 0
│   ├── CPURangeIterArrayPtr:             Operation: 0     Gas: 0
│   └── CPUReturnCallDefers:              Operation: 0     Gas: 0
├── KVStore:                              Operation: 8     Gas: 88384
│   ├── KVStoreGetObjectPerByte:          Operation: 6     Gas: 45344
│   ├── KVStoreSetObjectPerByte:          Operation: 1     Gas: 3216
│   ├── KVStoreGetTypePerByte:            Operation: 0     Gas: 0
│   ├── KVStoreSetTypePerByte:            Operation: 0     Gas: 0
│   ├── KVStoreGetPackageRealmPerByte:    Operation: 1     Gas: 39824
│   ├── KVStoreSetPackageRealmPerByte:    Operation: 0     Gas: 0
│   ├── KVStoreAddMemPackagePerByte:      Operation: 0     Gas: 0
│   ├── KVStoreGetMemPackagePerByte:      Operation: 0     Gas: 0
│   └── KVStoreDeleteObject:              Operation: 0     Gas: 0
├── Memory:                               Operation: 5     Gas: 1192
│   ├── MemoryAllocPerByte:               Operation: 5     Gas: 1192
│   └── MemoryGarbageCollect:             Operation: 0     Gas: 0
├── Native:                               Operation: 0     Gas: 0
│   ├── NativePrintFlat:                  Operation: 0     Gas: 0
│   └── NativePrintPerByte:               Operation: 0     Gas: 0
├── Parsing:                              Operation: 14    Gas: 23
│   ├── ParsingToken:                     Operation: 8     Gas: 8
│   └── ParsingNesting:                   Operation: 6     Gas: 15
├── Store:                                Operation: 30    Gas: 40700
│   ├── StoreReadFlat:                    Operation: 10    Gas: 10000
│   ├── StoreReadPerByte:                 Operation: 10    Gas: 3450
│   ├── StoreWriteFlat:                   Operation: 5     Gas: 10000
│   ├── StoreWritePerByte:                Operation: 5     Gas: 17250
│   ├── StoreHas:                         Operation: 0     Gas: 0
│   ├── StoreDelete:                      Operation: 0     Gas: 0
│   ├── StoreIterNextFlat:                Operation: 0     Gas: 0
│   └── StoreValuePerByte:                Operation: 0     Gas: 0
├── Testing:                              Operation: 0     Gas: 0
│   └── Testing:                          Operation: 0     Gas: 0
└── Transaction:                          Operation: 2     Gas: 3440
    ├── TransactionPerByte:               Operation: 1     Gas: 2440
    ├── TransactionSigVerifyEd25519:      Operation: 0     Gas: 0
    └── TxansactionSigVerifySecp256k1:    Operation: 1     Gas: 1000

HEIGHT:     32
EVENTS:     []
INFO:
TX HASH:    RQ9UomnOrU9bZQGiD/nmakDyJfd0hR8UllAcwFd7hcc=

Questions

  • Is it okay to have added float support to the overflow package? Or should we have created a new package for that? (Since float operations don't really overflow in the same way)
  • Should we remove the CPU Cycles metric in favor of a more relevant one or more detailed logging?
  • The only place where gas costs were not a constant hardcoded somewhere in the codebase is the auth package in tm2/pkg/sdk/auth/params.go. Except for the default values, these gas costs were set by the genesis file.
    Does it make sense to set some costs through the genesis while others are defined in a config struct somewhere in the codebase? Should we make all costs configurable via the genesis file, or remove all cost parameters from the genesis (as I did in this PR)?

@aeddi aeddi self-assigned this Dec 9, 2025
@github-actions github-actions bot added 📦 🤖 gnovm Issues or PRs gnovm related 📦 🌐 tendermint v2 Issues or PRs tm2 related 📦 ⛰️ gno.land Issues or PRs gno.land package related 🐹 golang Pull requests that update Go code 🛠️ gnodev labels Dec 9, 2025
@Gno2D2
Copy link
Collaborator

Gno2D2 commented Dec 9, 2025

🛠 PR Checks Summary

All Automated Checks passed. ✅

Manual Checks (for Reviewers):
  • IGNORE the bot requirements for this PR (force green CI check)
Read More

🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers.

✅ Automated Checks (for Contributors):

🟢 Maintainers must be able to edit this pull request (more info)

☑️ Contributor Actions:
  1. Fix any issues flagged by automated checks.
  2. Follow the Contributor Checklist to ensure your PR is ready for review.
    • Add new tests, or document why they are unnecessary.
    • Provide clear examples/screenshots, if necessary.
    • Update documentation, if required.
    • Ensure no breaking changes, or include BREAKING CHANGE notes.
    • Link related issues/PRs, where applicable.
☑️ Reviewer Actions:
  1. Complete manual checks for the PR, including the guidelines and additional checks if applicable.
📚 Resources:
Debug
Automated Checks
Maintainers must be able to edit this pull request (more info)

If

🟢 Condition met
└── 🟢 And
    ├── 🟢 The base branch matches this pattern: ^master$
    └── 🟢 The pull request was created from a fork (head branch repo: aeddi/gno)

Then

🟢 Requirement satisfied
└── 🟢 Maintainer can modify this pull request

Manual Checks
**IGNORE** the bot requirements for this PR (force green CI check)

If

🟢 Condition met
└── 🟢 On every pull request

Can be checked by

  • Any user with comment edit permission

@Kouteki Kouteki moved this from Triage to In Review in 🧙‍♂️Gno.land development Dec 12, 2025
metrics.VMCPUCycles.Record(
context.Background(),
cpuCycles,
gasUsed.CategoryDetails()["CPU"].Total.GasConsumed,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CategoryDetails is used to read the CPU total, but this method rebuild the entire map. So it seems like the map construction costs and memory allocation would occur every time the logTelemetry function is called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, indeed, but since this function is called only once per maketx addPkg / call / run, it seems okay to me to build the map at that moment so we keep the metering as fast as possible.

@notJoon
Copy link
Member

notJoon commented Dec 16, 2025

Is it okay to have added float support to the overflow package? Or should we have created a new package for that? (Since float operations don't really overflow in the same way)

Floats don’t overflow like integers, so putting them in the same Add/Sub helpers can surprise callers who expect integer semantics. It’d be cleaner to keep the current PR as-is but split float-safe helpers into a separate function or package so the integer overflow helpers stay semantically tight.

Should we remove the CPU Cycles metric in favor of a more relevant one or more detailed logging?

The current CPU metric is actually CPU gas rather than hardware cycles, the name could be misleading (I was confused by this initially as well). Rather than removing it entirely, renaming it to have a clearer name?

The only place where gas costs were not a constant hardcoded somewhere in the codebase [...]

mixing auth params with hardcoded value elsewhere would reduce consistency. It seems better to choose one model and stick with it, but deciding which to choose is beyond my hands

Copy link
Member

@gfanton gfanton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking good, only small typo

Co-authored-by: Guilhem Fanton <8671905+gfanton@users.noreply.github.com>
@gfanton gfanton self-requested a review January 23, 2026 09:07
@aeddi aeddi mentioned this pull request Jan 23, 2026
4 tasks
@Kouteki Kouteki added this to the 🚀 Mainnet beta launch milestone Jan 31, 2026
Home string
Remote string
Quiet bool
Verbosity int // Verbosity level for gas detail: 0=none, 1=categories, 2=non-zero ops, 3=all ops
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Verbosity int // Verbosity level for gas detail: 0=none, 1=categories, 2=non-zero ops, 3=all ops
Verbosity gas.Verbosity

Should we create a specific enum for this?

type Verbosity int

const (
	VerbosityNone       Verbosity = iota // No gas detail
	VerbosityCategories                  // Show gas categories
	VerbosityNonZeroOps                  // Show non-zero ops
	VerbosityAllOps                      // Show all ops
)


// Usage
// gas.VerbosityNone

Comment on lines +21 to +30
func PrintTxInfo(tx std.Tx, res *ctypes.ResultBroadcastTxCommit, io commands.IO, verbosity int) {
io.Println(string(res.DeliverTx.Data))
io.Println("OK!")
io.Println("GAS WANTED:", res.DeliverTx.GasWanted)

if verbosity == 0 {
io.Println("GAS USED: ", res.DeliverTx.GasUsed.Total.GasConsumed)
} else {
printGasDetail(res.DeliverTx.GasUsed, io, verbosity)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example of a Gas logger, to avoid having all the if-else logic to determine what should be printed and what shouldn't

Suggested change
func PrintTxInfo(tx std.Tx, res *ctypes.ResultBroadcastTxCommit, io commands.IO, verbosity int) {
io.Println(string(res.DeliverTx.Data))
io.Println("OK!")
io.Println("GAS WANTED:", res.DeliverTx.GasWanted)
if verbosity == 0 {
io.Println("GAS USED: ", res.DeliverTx.GasUsed.Total.GasConsumed)
} else {
printGasDetail(res.DeliverTx.GasUsed, io, verbosity)
}
func PrintTxInfo(tx std.Tx, res *ctypes.ResultBroadcastTxCommit, io commands.IO, verbosity Verbosity) {
vp := NewGasLogger(verbosity, io.Out())
vp.Always(string(res.DeliverTx.Data))
vp.Always("OK!")
vp.Always("GAS WANTED:", res.DeliverTx.GasWanted)
vp.Always("GAS USED: ", res.DeliverTx.GasUsed.Total.GasConsumed)
printGasDetail(res.DeliverTx.GasUsed, io, verbosity)

Possible Methods:

func (gl *GasLogger) Category(...)  // Verbosity >= 1
func (gl *GasLogger) Op(...)             // Verbosity >= 2
func (gl *GasLogger) OpDetail(...)  // Verbosity >= 3
func (gl *GasLogger) Always(...)     // Ignores Verbosity or Verbosity == 0

thehowl pushed a commit that referenced this pull request Feb 3, 2026
Resolve issue comment
#1998 (comment) .

To show the gas used in `gno test` results:

* For "*filetest.gno", `runFiletest` already creates a virtual machine
[with a gas
meter](https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/filetest.go#L77).
We update `runFiletest` to add a return value for
`m.GasMeter.GasConsumed()`
* When `Test` calls `runFiletest`, get and display the gas used
* For normal "*.gno" tests, `runTestFiles` [creates a virtual
machine](https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/test.go#L401https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/test.go#L401)
by calling `Machine`, where we add an optional param `gasMeter`. In
`runTestFiles` we set this to `store.NewInfiniteGasMeter()` .
* After `runTestFiles` uses `m.Eval` to evaluate the gno test, it prints
the gas used
* Update tests to expect the gas in the output like `(elapsed: 0.01s,
gas: 648732)`

Sample output:
```
=== RUN   TestTreeReverseIterateByOffset
--- PASS: TestTreeReverseIterateByOffset (0.00s)
--- GAS:  218576
=== RUN   ./z_0_filetest.gno
false 2
--- PASS: ./z_0_filetest.gno (elapsed: 0.01s, gas: 648732)
```

(The CI check for codecov often fails for the filetest infrastructure.
Suggest to ignore.)

In the future, when #4953 is merged,
then we can integrate tests with this detail.

---------

Signed-off-by: Jeff Thompson <jeff@thefirst.org>
Co-authored-by: ltzmaxwell <ltz.maxwell@gmail.com>
@Kouteki Kouteki removed the request for review from zivkovicmilos February 15, 2026 22:10
@gfanton gfanton mentioned this pull request Feb 16, 2026
Copy link
Member

@thehowl thehowl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a quick review, trying to look at the implementation from the macro level.

Responding from the top level comment:

  • Move gas meter to a dedicated package: good
  • Centralize gas operations: yes please, but I would actually keep a distinction per-module. (ie. tm2 gas, gnovm gas, gno.land gas - gno.land uses a gas cost combination of all 3). Also, as I noted, float64 should be banned almost entirely in tm2/
  • Should we remove the CPU Cycles metric in favor of a more relevant one or more detailed logging?
    • I would remove CPU cycles from the VM, yes, it's largely useless now
  • Does it make sense to set some costs through the genesis while others are defined in a config struct somewhere in the codebase? Should we make all costs configurable via the genesis file, or remove all cost parameters from the genesis (as I did in this PR)?
    • Gas costs should be configurable by the chain somehow, to allow live chain changes without having to perform a full upgrade if we get some values wrong.

package gas

// Cost represents the gas cost for an operation.
type Cost float64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't calculate gas using floats.

Floats have a risk to be unpredictable (gno uses softfloat for this exact reason; so the consensus shouldn't use floating point calculation either). Instead, let's use a ratio (fraction):

type Ratio struct { Num, Denom int64 }

// Floored multiplication
func (r Ratio) Mul(n int64) int64 { return (n * r.Num) / r.Denom }

// Ceiled multiplication
func (r Ratio) MulCeil(n int64) int64 { return (n * r.Num + (r.Denom-1)) / r.Denom }

// Multiply by other ratio
func (r Ratio) MulRatio(s Ratio) Ratio {
	// GCD'ing allows to keep ratio numbers small.
	gcdA, gcdB := gcd(r.Num, s.Denom), gcd(s.Num, r.Denom)
	return Ratio{(r.Num / gcdA) * (s.Num / gcdB), (r.Denom / gcdB) * (s.Denom / gcdA)}
}

Name and implementations can be changed (maybe we should add overflow-checking), but the underlying idea is to make this work using only integer arithmetic. Gas is already expressed as a ratio anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the spirit of what I previously mentioned, overflow shouldn't support floating points.

ResponseBase
GasWanted int64
GasUsed int64
GasUsed gas.GasDetail
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GasDetail is a big type. It is 512 * 8 bytes = 4kb in memory. These have to be listed every time on the wire, even when they're 0. Furthermore, I believe it is even stored in the block data in the database, and while I believe amino uses uvarint, it is still a lot of bytes.

I think this should not be changed. We can change sdk.Result; we can store the gas in the database, but changing ResponseDeliverTx from what I understand has too many implications.

jaekwon pushed a commit that referenced this pull request Feb 24, 2026
Resolve issue comment
#1998 (comment) .

To show the gas used in `gno test` results:

* For "*filetest.gno", `runFiletest` already creates a virtual machine
[with a gas
meter](https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/filetest.go#L77).
We update `runFiletest` to add a return value for
`m.GasMeter.GasConsumed()`
* When `Test` calls `runFiletest`, get and display the gas used
* For normal "*.gno" tests, `runTestFiles` [creates a virtual
machine](https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/test.go#L401https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/test.go#L401)
by calling `Machine`, where we add an optional param `gasMeter`. In
`runTestFiles` we set this to `store.NewInfiniteGasMeter()` .
* After `runTestFiles` uses `m.Eval` to evaluate the gno test, it prints
the gas used
* Update tests to expect the gas in the output like `(elapsed: 0.01s,
gas: 648732)`

Sample output:
```
=== RUN   TestTreeReverseIterateByOffset
--- PASS: TestTreeReverseIterateByOffset (0.00s)
--- GAS:  218576
=== RUN   ./z_0_filetest.gno
false 2
--- PASS: ./z_0_filetest.gno (elapsed: 0.01s, gas: 648732)
```

(The CI check for codecov often fails for the filetest infrastructure.
Suggest to ignore.)

In the future, when #4953 is merged,
then we can integrate tests with this detail.

---------

Signed-off-by: Jeff Thompson <jeff@thefirst.org>
Co-authored-by: ltzmaxwell <ltz.maxwell@gmail.com>
jaekwon pushed a commit that referenced this pull request Feb 24, 2026
Resolve issue comment
#1998 (comment) .

To show the gas used in `gno test` results:

* For "*filetest.gno", `runFiletest` already creates a virtual machine
[with a gas
meter](https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/filetest.go#L77).
We update `runFiletest` to add a return value for
`m.GasMeter.GasConsumed()`
* When `Test` calls `runFiletest`, get and display the gas used
* For normal "*.gno" tests, `runTestFiles` [creates a virtual
machine](https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/test.go#L401https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/test.go#L401)
by calling `Machine`, where we add an optional param `gasMeter`. In
`runTestFiles` we set this to `store.NewInfiniteGasMeter()` .
* After `runTestFiles` uses `m.Eval` to evaluate the gno test, it prints
the gas used
* Update tests to expect the gas in the output like `(elapsed: 0.01s,
gas: 648732)`

Sample output:
```
=== RUN   TestTreeReverseIterateByOffset
--- PASS: TestTreeReverseIterateByOffset (0.00s)
--- GAS:  218576
=== RUN   ./z_0_filetest.gno
false 2
--- PASS: ./z_0_filetest.gno (elapsed: 0.01s, gas: 648732)
```

(The CI check for codecov often fails for the filetest infrastructure.
Suggest to ignore.)

In the future, when #4953 is merged,
then we can integrate tests with this detail.

---------

Signed-off-by: Jeff Thompson <jeff@thefirst.org>
Co-authored-by: ltzmaxwell <ltz.maxwell@gmail.com>
thehowl pushed a commit to thehowl/gno that referenced this pull request Feb 24, 2026
Resolve issue comment
gnolang#1998 (comment) .

To show the gas used in `gno test` results:

* For "*filetest.gno", `runFiletest` already creates a virtual machine
[with a gas
meter](https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/filetest.go#L77).
We update `runFiletest` to add a return value for
`m.GasMeter.GasConsumed()`
* When `Test` calls `runFiletest`, get and display the gas used
* For normal "*.gno" tests, `runTestFiles` [creates a virtual
machine](https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/test.go#L401https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/test.go#L401)
by calling `Machine`, where we add an optional param `gasMeter`. In
`runTestFiles` we set this to `store.NewInfiniteGasMeter()` .
* After `runTestFiles` uses `m.Eval` to evaluate the gno test, it prints
the gas used
* Update tests to expect the gas in the output like `(elapsed: 0.01s,
gas: 648732)`

Sample output:
```
=== RUN   TestTreeReverseIterateByOffset
--- PASS: TestTreeReverseIterateByOffset (0.00s)
--- GAS:  218576
=== RUN   ./z_0_filetest.gno
false 2
--- PASS: ./z_0_filetest.gno (elapsed: 0.01s, gas: 648732)
```

(The CI check for codecov often fails for the filetest infrastructure.
Suggest to ignore.)

In the future, when gnolang#4953 is merged,
then we can integrate tests with this detail.

---------

Signed-off-by: Jeff Thompson <jeff@thefirst.org>
Co-authored-by: ltzmaxwell <ltz.maxwell@gmail.com>
jefft0 added a commit to jefft0/gno that referenced this pull request Feb 25, 2026
Resolve issue comment
gnolang#1998 (comment) .

To show the gas used in `gno test` results:

* For "*filetest.gno", `runFiletest` already creates a virtual machine
[with a gas
meter](https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/filetest.go#L77).
We update `runFiletest` to add a return value for
`m.GasMeter.GasConsumed()`
* When `Test` calls `runFiletest`, get and display the gas used
* For normal "*.gno" tests, `runTestFiles` [creates a virtual
machine](https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/test.go#L401https://github.com/gnolang/gno/blob/d4e083b4d2cd2ba8da9740ebb2dec1ba503b4108/gnovm/pkg/test/test.go#L401)
by calling `Machine`, where we add an optional param `gasMeter`. In
`runTestFiles` we set this to `store.NewInfiniteGasMeter()` .
* After `runTestFiles` uses `m.Eval` to evaluate the gno test, it prints
the gas used
* Update tests to expect the gas in the output like `(elapsed: 0.01s,
gas: 648732)`

Sample output:
```
=== RUN   TestTreeReverseIterateByOffset
--- PASS: TestTreeReverseIterateByOffset (0.00s)
--- GAS:  218576
=== RUN   ./z_0_filetest.gno
false 2
--- PASS: ./z_0_filetest.gno (elapsed: 0.01s, gas: 648732)
```

(The CI check for codecov often fails for the filetest infrastructure.
Suggest to ignore.)

In the future, when gnolang#4953 is merged,
then we can integrate tests with this detail.

---------

Signed-off-by: Jeff Thompson <jeff@thefirst.org>
Co-authored-by: ltzmaxwell <ltz.maxwell@gmail.com>
@jaekwon
Copy link
Contributor

jaekwon commented Mar 14, 2026

See also #5289; may need to rebase.

@jaekwon jaekwon self-requested a review March 15, 2026 02:03
@jaekwon
Copy link
Contributor

jaekwon commented Mar 15, 2026

Take a look at #5289 refactor(benchops): gap-free timing model and measurement fixes and
#5091 fix(gnovm): proper gas consumption for mem allocation and
#5291: Gas benchmark overhaul.

I will need to review all of these before they are merged (don't merge w/o me). Please consider splitting the PR and on top of the above three.

The third one will change the way gas charging is done for opcodes that need parameterization: and there are many.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐳 devops 🛠️ gnodev 🐹 golang Pull requests that update Go code 📦 🌐 tendermint v2 Issues or PRs tm2 related 📦 ⛰️ gno.land Issues or PRs gno.land package related 📦 🤖 gnovm Issues or PRs gnovm related

Projects

Status: In Progress
Status: In Review

Development

Successfully merging this pull request may close these issues.

8 participants