feat(gasmeter): support detailed metering and reporting
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.NewGasMeter → gas.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 ingnovm/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 ingnovm/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 intm2/pkg/sdk/auth/params.go
After this commit:
- Everything is centralized inside the gas package with operations in
tm2/pkg/gas/operation.goand the associated costs intm2/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)).
- If a cost is flat (like for a CPU opcode operation), the usage is
- Cost and multiplier are both
float64to allow division through this simple API (multiply by 0.5 to get half, etc.), but the gas counting still usesint64.
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)?
🛠 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:
- Fix any issues flagged by automated checks.
- 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 CHANGEnotes. - Link related issues/PRs, where applicable.
☑️ Reviewer Actions:
- 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 requestManual Checks
**IGNORE** the bot requirements for this PR (force green CI check)
If
🟢 Condition met └── 🟢 On every pull requestCan be checked by
- Any user with comment edit permission
Codecov Report
:x: Patch coverage is 94.00428% with 28 lines in your changes missing coverage. Please review.
:loudspeaker: Thoughts on this report? Let us know!
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