PulseSchedulerGasBenchmark.t.sol 8.1 KB

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