PulseSchedulerGasBenchmark.t.sol 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. // SPDX-License-Identifier: Apache 2
  2. pragma solidity ^0.8.0;
  3. import "forge-std/Test.sol";
  4. import "forge-std/console.sol";
  5. import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
  6. import "@openzeppelin/contracts/utils/math/SafeCast.sol";
  7. import "../contracts/pulse/SchedulerUpgradeable.sol";
  8. import "@pythnetwork/pulse-sdk-solidity/IScheduler.sol";
  9. import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol";
  10. import "@pythnetwork/pulse-sdk-solidity/SchedulerEvents.sol";
  11. import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol";
  12. import "./utils/PulseSchedulerTestUtils.t.sol";
  13. contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils {
  14. ERC1967Proxy public proxy;
  15. SchedulerUpgradeable public scheduler;
  16. address public manager;
  17. address public admin;
  18. address public pyth;
  19. function setUp() public {
  20. manager = address(1);
  21. admin = address(2);
  22. pyth = address(3);
  23. uint128 minBalancePerFeed = 10 ** 16; // 0.01 ether
  24. uint128 keeperFee = 10 ** 15; // 0.001 ether
  25. SchedulerUpgradeable _scheduler = new SchedulerUpgradeable();
  26. proxy = new ERC1967Proxy(
  27. address(_scheduler),
  28. abi.encodeWithSelector(
  29. SchedulerUpgradeable.initialize.selector,
  30. manager,
  31. admin,
  32. pyth,
  33. minBalancePerFeed,
  34. keeperFee
  35. )
  36. );
  37. scheduler = SchedulerUpgradeable(address(proxy));
  38. // Start tests at a high timestamp to avoid underflow when we set
  39. // `minPublishTime = timestamp - 1 hour` in updatePriceFeeds
  40. vm.warp(100000);
  41. // Give manager 1000 ETH for testing
  42. vm.deal(manager, 1000 ether);
  43. }
  44. // Helper function to run the price feed update benchmark with a specified number of feeds
  45. function _runUpdateAndQueryPriceFeedsBenchmark(uint8 numFeeds) internal {
  46. // Setup: Create subscription and perform initial update
  47. vm.prank(manager);
  48. uint256 subscriptionId = _setupSubscriptionWithInitialUpdate(numFeeds);
  49. (SchedulerStructs.SubscriptionParams memory params, ) = scheduler
  50. .getSubscription(subscriptionId);
  51. // Advance time to meet heartbeat criteria
  52. vm.warp(block.timestamp + 100);
  53. // Create new price feed updates with updated timestamp
  54. uint64 newPublishTime = SafeCast.toUint64(block.timestamp);
  55. PythStructs.PriceFeed[] memory newPriceFeeds;
  56. uint64[] memory newSlots;
  57. (newPriceFeeds, newSlots) = createMockPriceFeedsWithSlots(
  58. newPublishTime,
  59. numFeeds
  60. );
  61. // Mock Pyth response for the benchmark
  62. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, newPriceFeeds, newSlots);
  63. // Actual benchmark: Measure gas for updating price feeds
  64. uint256 startGas = gasleft();
  65. scheduler.updatePriceFeeds(
  66. subscriptionId,
  67. createMockUpdateData(newPriceFeeds)
  68. );
  69. uint256 updateGasUsed = startGas - gasleft();
  70. console.log(
  71. "Gas used for updating %s feeds: %s",
  72. vm.toString(numFeeds),
  73. vm.toString(updateGasUsed)
  74. );
  75. // Benchmark querying the price feeds after updating
  76. uint256 queryStartGas = gasleft();
  77. scheduler.getPricesUnsafe(subscriptionId, params.priceIds);
  78. uint256 queryGasUsed = queryStartGas - gasleft();
  79. console.log(
  80. "Gas used for querying %s feeds: %s",
  81. vm.toString(numFeeds),
  82. vm.toString(queryGasUsed)
  83. );
  84. console.log(
  85. "Total gas used for updating and querying %s feeds: %s",
  86. vm.toString(numFeeds),
  87. vm.toString(updateGasUsed + queryGasUsed)
  88. );
  89. }
  90. // Helper function to set up a subscription with initial price update
  91. function _setupSubscriptionWithInitialUpdate(
  92. uint8 numFeeds
  93. ) internal returns (uint256) {
  94. uint256 subscriptionId = addTestSubscriptionWithFeeds(
  95. scheduler,
  96. numFeeds,
  97. address(manager)
  98. );
  99. // Create initial price feed updates
  100. uint64 publishTime = SafeCast.toUint64(block.timestamp);
  101. PythStructs.PriceFeed[] memory priceFeeds;
  102. uint64[] memory slots;
  103. (priceFeeds, slots) = createMockPriceFeedsWithSlots(
  104. publishTime,
  105. numFeeds
  106. );
  107. mockParsePriceFeedUpdatesWithSlotsStrict(pyth, priceFeeds, slots);
  108. bytes[] memory updateData = createMockUpdateData(priceFeeds);
  109. // Update the price feeds. We should have enough balance to cover the update
  110. // because we funded the subscription with the minimum balance during creation.
  111. scheduler.updatePriceFeeds(subscriptionId, updateData);
  112. return subscriptionId;
  113. }
  114. // Helper function to create updated price feeds for benchmark
  115. function _createUpdatedPriceFeeds(
  116. uint8 numFeeds
  117. ) internal returns (PythStructs.PriceFeed[] memory, uint64[] memory) {}
  118. /// Helper function for benchmarking querying active subscriptions with a specified number of total subscriptions.
  119. /// Half of them will be inactive to simulate gaps in the subscriptions list.
  120. /// Keepers will poll this function to get the list of active subscriptions.
  121. function _runGetActiveSubscriptionsBenchmark(
  122. uint256 numSubscriptions
  123. ) internal {
  124. // Setup: As manager, create subscriptions and then deactivate every other one.
  125. vm.startPrank(manager);
  126. // Array to store subscription IDs
  127. uint256[] memory subscriptionIds = new uint256[](numSubscriptions);
  128. // First create all subscriptions as active (with default 2 price feeds)
  129. for (uint256 i = 0; i < numSubscriptions; i++) {
  130. subscriptionIds[i] = addTestSubscription(
  131. scheduler,
  132. address(manager)
  133. );
  134. }
  135. // Deactivate every other subscription
  136. for (uint256 i = 0; i < numSubscriptions; i++) {
  137. if (i % 2 == 1) {
  138. (
  139. SchedulerStructs.SubscriptionParams memory params,
  140. ) = scheduler.getSubscription(subscriptionIds[i]);
  141. params.isActive = false;
  142. scheduler.updateSubscription(subscriptionIds[i], params);
  143. }
  144. }
  145. vm.stopPrank();
  146. // Actual benchmark: Measure gas for fetching active subscriptions
  147. uint256 startGas = gasleft();
  148. scheduler.getActiveSubscriptions(0, numSubscriptions);
  149. uint256 gasUsed = startGas - gasleft();
  150. console.log(
  151. "Gas used for fetching %s active subscriptions out of %s total: %s",
  152. vm.toString((numSubscriptions + 1) / 2),
  153. vm.toString(numSubscriptions),
  154. vm.toString(gasUsed)
  155. );
  156. }
  157. // Benchmark tests for the basic flow: updating and reading price feeds with different feed counts
  158. // NOTE: run these tests with -vv to see the gas usage for the operations under test, without setup costs
  159. function testUpdateAndQueryPriceFeeds01Feed() public {
  160. _runUpdateAndQueryPriceFeedsBenchmark(1);
  161. }
  162. function testUpdateAndQueryPriceFeeds02Feeds() public {
  163. _runUpdateAndQueryPriceFeedsBenchmark(2);
  164. }
  165. function testUpdateAndQueryPriceFeeds04Feeds() public {
  166. _runUpdateAndQueryPriceFeedsBenchmark(4);
  167. }
  168. function testUpdateAndQueryPriceFeeds08Feeds() public {
  169. _runUpdateAndQueryPriceFeedsBenchmark(8);
  170. }
  171. function testUpdateAndQueryPriceFeeds10Feeds() public {
  172. _runUpdateAndQueryPriceFeedsBenchmark(10);
  173. }
  174. function testUpdateAndQueryPriceFeeds20Feeds() public {
  175. _runUpdateAndQueryPriceFeedsBenchmark(20);
  176. }
  177. // Benchmark tests for fetching active subscriptions with different counts
  178. // NOTE: run these tests with -vv to see the gas usage for the operations under test, without setup costs
  179. function testGetActiveSubscriptions010() public {
  180. _runGetActiveSubscriptionsBenchmark(10);
  181. }
  182. function testGetActiveSubscriptions100() public {
  183. _runGetActiveSubscriptionsBenchmark(100);
  184. }
  185. function testGetActiveSubscriptions1000() public {
  186. _runGetActiveSubscriptionsBenchmark(1000);
  187. }
  188. // Allow the contract to receive Ether (for keeper payments during tests)
  189. receive() external payable {}
  190. }