gno icon indicating copy to clipboard operation
gno copied to clipboard

feat(gasmeter): support detailed metering and reporting

Open aeddi opened this issue 2 weeks ago • 2 comments

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 (https://github.com/gnolang/gno/commit/a9f9eeba526db8d605c9156528117b259018f473)

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 (https://github.com/gnolang/gno/commit/00cb42e3c097ce0220b2a769048b2dfb189248a9)

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 (https://github.com/gnolang/gno/commit/ebdf7a101bf1b9b4873ba9663123dde40a3373f4)

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 (https://github.com/gnolang/gno/commit/dd954ff3bb3715ec91d390fb26ac5f5a19546cd9)

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 (https://github.com/gnolang/gno/commit/07b6cf2604d224f0959fd84656f48ce522543531)

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 (https://github.com/gnolang/gno/commit/92b7b86c35ca4a75b74b968520727fe28473fde0)

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 avatar Dec 09 '25 13:12 aeddi

🛠 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

Gno2D2 avatar Dec 09 '25 13:12 Gno2D2

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

notJoon avatar Dec 16 '25 09:12 notJoon