Skip to content

Commit 1a9dfb6

Browse files
authored
[eth]: Fix gas benchmark to generate useful gas snapshot (#372)
* [eth]: Fix incorrect gas usage problem * Make gas report more accurate * Update readme * Address Jayant comment
1 parent 2c542f9 commit 1a9dfb6

File tree

2 files changed

+59
-67
lines changed

2 files changed

+59
-67
lines changed

ethereum/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,19 @@ A gas report should have a couple of tables like this:
5959
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
6060
│ ............. ┆ ..... ┆ ..... ┆ ..... ┆ ..... ┆ .. │
6161
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
62-
│ parseAndVerifyVM ┆ 90292 ┆ 91262 ┆ 90292 ┆ 138792 ┆ 50 │
63-
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
64-
│ updatePriceFeeds ┆ 187385 ┆ 206005 ┆ 187385 ┆ 1118385 ┆ 50 │
62+
│ updatePriceFeeds ┆ 383169 ┆ 724277 ┆ 187385 ┆ 1065385 ┆ 2 │
6563
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
6664
│ ............. ┆ ..... ┆ ..... ┆ ..... ┆ ..... ┆ ... │
6765
╰───────────────────────────────────────────────────────────────────────────────────────────┴─────────────────┴────────┴────────┴─────────┴─────────╯
6866
```
6967

70-
For most of the methods, the median gas usage is an indication of our desired gas usage. Because the calls that store something in the storage
71-
for the first time use significantly more gas.
68+
For most of the methods, the minimum gas usage is an indication of our desired gas usage. Because the calls that store something in the storage
69+
for the first time in `setUp` use significantly more gas. For example, in the above table, there are two calls to `updatePriceFeeds`. The first
70+
call has happend in the `setUp` method and costed over a million gas and is not intended for our Benchmark. So our desired value is the
71+
minimum value which is around 380k gas.
7272

7373
If you like to optimize the contract and measure the gas optimization you can get gas snapshots using `forge snapshot` and evaluate your
7474
optimization with it. For more information, please refer to [Gas Snapshots documentation](https://book.getfoundry.sh/forge/gas-snapshots).
7575
Once you optimized the code, please share the snapshot difference (generated using `forge snapshot --diff <old-snapshot>`) in the PR too.
76+
This snapshot gas value also includes an initial transaction cost as well as reading from the contract storage itself. You can get the
77+
most accurate result by looking at the gas report or the gas shown in the call trace with `-vvvv` argument to `forge test`.

ethereum/forge-test/GasBenchmark.t.sol

Lines changed: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,23 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
2121
// We use 5 prices to form a batch of 5 prices, close to our mainnet transactions.
2222
uint8 constant NUM_PRICES = 5;
2323

24-
uint constant BENCHMARK_ITERATIONS = 1000;
25-
2624
IPyth public pyth;
2725

2826
bytes32[] priceIds;
29-
PythStructs.Price[] prices;
27+
28+
// Cached prices are populated in the setUp
29+
PythStructs.Price[] cachedPrices;
30+
bytes[] cachedPricesUpdateData;
31+
uint cachedPricesUpdateFee;
32+
uint64[] cachedPricesPublishTimes;
33+
34+
// Fresh prices are different prices that can be used
35+
// as a fresh price to update the prices
36+
PythStructs.Price[] freshPrices;
37+
bytes[] freshPricesUpdateData;
38+
uint freshPricesUpdateFee;
39+
uint64[] freshPricesPublishTimes;
40+
3041
uint64 sequence;
3142
uint randSeed;
3243

@@ -40,29 +51,40 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
4051
}
4152

4253
for (uint i = 0; i < NUM_PRICES; ++i) {
43-
prices.push(PythStructs.Price(
54+
55+
uint64 publishTime = uint64(getRand() % 10);
56+
57+
cachedPrices.push(PythStructs.Price(
58+
int64(uint64(getRand() % 1000)), // Price
59+
uint64(getRand() % 100), // Confidence
60+
-5, // Expo
61+
publishTime
62+
));
63+
cachedPricesPublishTimes.push(publishTime);
64+
65+
publishTime += uint64(getRand() % 10);
66+
freshPrices.push(PythStructs.Price(
4467
int64(uint64(getRand() % 1000)), // Price
4568
uint64(getRand() % 100), // Confidence
4669
-5, // Expo
47-
getRand() % 10 // publishTime
70+
publishTime
4871
));
72+
freshPricesPublishTimes.push(publishTime);
4973
}
74+
75+
// Populate the contract with the initial prices
76+
(cachedPricesUpdateData, cachedPricesUpdateFee) = generateUpdateDataAndFee(cachedPrices);
77+
pyth.updatePriceFeeds{value: cachedPricesUpdateFee}(cachedPricesUpdateData);
78+
79+
(freshPricesUpdateData, freshPricesUpdateFee) = generateUpdateDataAndFee(freshPrices);
5080
}
5181

5282
function getRand() internal returns (uint val) {
5383
++randSeed;
5484
val = uint(keccak256(abi.encode(randSeed)));
5585
}
5686

57-
function advancePrices() internal {
58-
for (uint i = 0; i < NUM_PRICES; ++i) {
59-
prices[i].price = int64(uint64(getRand() % 1000));
60-
prices[i].conf = uint64(getRand() % 100);
61-
prices[i].publishTime += getRand() % 10;
62-
}
63-
}
64-
65-
function generateUpdateDataAndFee() internal returns (bytes[] memory updateData, uint updateFee) {
87+
function generateUpdateDataAndFee(PythStructs.Price[] memory prices) internal returns (bytes[] memory updateData, uint updateFee) {
6688
bytes memory vaa = generatePriceFeedUpdateVAA(
6789
priceIds,
6890
prices,
@@ -79,68 +101,36 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
79101
}
80102

81103
function testBenchmarkUpdatePriceFeedsFresh() public {
82-
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
83-
advancePrices();
84-
85-
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
86-
pyth.updatePriceFeeds{value: updateFee}(updateData);
87-
}
104+
pyth.updatePriceFeeds{value: freshPricesUpdateFee}(freshPricesUpdateData);
88105
}
89106

90107
function testBenchmarkUpdatePriceFeedsNotFresh() public {
91-
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
92-
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
93-
pyth.updatePriceFeeds{value: updateFee}(updateData);
94-
}
108+
pyth.updatePriceFeeds{value: cachedPricesUpdateFee}(cachedPricesUpdateData);
95109
}
96110

97111
function testBenchmarkUpdatePriceFeedsIfNecessaryFresh() public {
98-
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
99-
advancePrices();
100-
101-
uint64[] memory publishTimes = new uint64[](NUM_PRICES);
102-
103-
for (uint j = 0; j < NUM_PRICES; ++j) {
104-
publishTimes[j] = uint64(prices[j].publishTime);
105-
}
106-
107-
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
108-
109-
// Since the prices have advanced, the publishTimes are newer than one in
110-
// the contract and hence, the call should succeed.
111-
pyth.updatePriceFeedsIfNecessary{value: updateFee}(updateData, priceIds, publishTimes);
112-
}
112+
// Since the prices have advanced, the publishTimes are newer than one in
113+
// the contract and hence, the call should succeed.
114+
pyth.updatePriceFeedsIfNecessary{value: freshPricesUpdateFee}(freshPricesUpdateData, priceIds, freshPricesPublishTimes);
113115
}
114116

115117
function testBenchmarkUpdatePriceFeedsIfNecessaryNotFresh() public {
116-
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
117-
uint64[] memory publishTimes = new uint64[](NUM_PRICES);
118-
119-
for (uint j = 0; j < NUM_PRICES; ++j) {
120-
publishTimes[j] = uint64(prices[j].publishTime);
121-
}
118+
// Since the price is not advanced, the publishTimes are the same as the
119+
// ones in the contract.
120+
vm.expectRevert(bytes("no prices in the submitted batch have fresh prices, so this update will have no effect"));
122121

123-
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
124-
125-
// Since the price is not advanced, the publishTimes are the same as the
126-
// ones in the contract except the first update.
127-
if (i > 0) {
128-
vm.expectRevert(bytes("no prices in the submitted batch have fresh prices, so this update will have no effect"));
129-
}
130-
131-
pyth.updatePriceFeedsIfNecessary{value: updateFee}(updateData, priceIds, publishTimes);
132-
}
122+
pyth.updatePriceFeedsIfNecessary{value: cachedPricesUpdateFee}(cachedPricesUpdateData, priceIds, cachedPricesPublishTimes);
133123
}
134124

135125
function testBenchmarkGetPrice() public {
136-
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
137-
pyth.updatePriceFeeds{value: updateFee}(updateData);
126+
// Set the block timestamp to 0. As prices have < 10 timestamp and staleness
127+
// is set to 60 seconds, the getPrice should work as expected.
128+
vm.warp(0);
138129

139-
// Set the block timestamp to the publish time, so getPrice work as expected.
140-
vm.warp(prices[0].publishTime);
130+
pyth.getPrice(priceIds[0]);
131+
}
141132

142-
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
143-
pyth.getPrice(priceIds[getRand() % NUM_PRICES]);
144-
}
133+
function testBenchmarkGetUpdateFee() public view {
134+
pyth.getUpdateFee(freshPricesUpdateData);
145135
}
146136
}

0 commit comments

Comments
 (0)