Skip to content

Commit

Permalink
feat: Аdd k6 performance tests for WS server (#2803)
Browse files Browse the repository at this point in the history
* fix: prep.js is not working with ethers v6

Signed-off-by: Victor Yanev <[email protected]>

* test: add k6 performance tests for WS server

Signed-off-by: Victor Yanev <[email protected]>

* fix: prep.js

Signed-off-by: Victor Yanev <[email protected]>

* fix: response checks

Signed-off-by: Victor Yanev <[email protected]>

* Update k6/src/prepare/contracts/Greeter.sol

Co-authored-by: Logan Nguyen <[email protected]>
Signed-off-by: Eric Badiere <[email protected]>

---------

Signed-off-by: Victor Yanev <[email protected]>
Signed-off-by: Eric Badiere <[email protected]>
Co-authored-by: Eric Badiere <[email protected]>
Co-authored-by: Logan Nguyen <[email protected]>
  • Loading branch information
3 people authored Sep 12, 2024
1 parent 8245f51 commit fe0c21f
Show file tree
Hide file tree
Showing 25 changed files with 433 additions and 30 deletions.
1 change: 1 addition & 0 deletions k6/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/prepare/.smartContractParams.json
10 changes: 10 additions & 0 deletions k6/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion k6/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
"scripts": {
"prep-and-run": "npm run prep && npm run k6",
"prep": "env-cmd node src/prepare/prep.js",
"k6": "env-cmd --use-shell k6 run src/scenarios/apis.js"
"k6": "env-cmd --use-shell k6 run src/scenarios/apis.js",
"k6-ws": "env-cmd --use-shell k6 run src/scenarios/ws-apis.js"
},
"dependencies": {
"env-cmd": "^10.1.0",
"ethers": "^6.13.2"
},
"devDependencies": {
"@types/k6": "^0.53.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import {
computeLatestEthereumTransactionParameters,
computeLatestLogParameters,
setDefaultValuesForEnvParameters,
} from '../../lib/parameters.js';
} from './parameters.js';

const scParams = JSON.parse(open('../../prepare/.smartContractParams.json'));
const scParams = JSON.parse(open('../prepare/.smartContractParams.json'));

const computeTestParameters = (configuration) =>
Object.assign(
Expand Down
7 changes: 7 additions & 0 deletions k6/src/lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ function markdownReport(data, isFirstColumnUrl, scenarios) {

// collect the metrics
const { metrics } = data;

const isDebugMode = __ENV['DEBUG_MODE'] === 'true';
if (isDebugMode) {
console.log("Raw metrics:");
console.log(JSON.stringify(metrics, null, 2));
}

const scenarioMetrics = {};

for (const [key, value] of Object.entries(metrics)) {
Expand Down
5 changes: 5 additions & 0 deletions k6/src/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@
export const logListName = 'logs';
export const resultListName = 'results';
export const transactionListName = 'transactions';
export const subscribeEvents = {
logs: 'logs',
newHeads: 'newHeads',
newPendingTransactions: 'newPendingTransactions',
};
24 changes: 24 additions & 0 deletions k6/src/prepare/contracts/Greeter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

contract Greeter {
string private greeting;

event GreetingSet(string greeting);

constructor(string memory _greeting) {
greeting = _greeting;

emit GreetingSet(_greeting);
}

function greet() public view returns (string memory) {
return greeting;
}

function setGreeting(string memory _greeting) public {
greeting = _greeting;

emit GreetingSet(_greeting);
}
}
53 changes: 36 additions & 17 deletions k6/src/prepare/prep.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
/*
*
* Hedera JSON RPC Relay
*
* Copyright (C) 2022-2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import Greeter from './contracts/Greeter.json' assert { type: 'json' };
import { ethers } from 'ethers';
import { ethers, formatEther, parseEther } from 'ethers';
import * as fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
Expand All @@ -9,8 +29,8 @@ const __dirname = path.dirname(__filename);

const logPayloads = process.env.DEBUG_MODE === 'true';

class LoggingProvider extends ethers.providers.JsonRpcProvider {
send(method, params) {
class LoggingProvider extends ethers.JsonRpcProvider {
async send(method, params) {
if (logPayloads) {
const request = {
method: method,
Expand Down Expand Up @@ -45,7 +65,7 @@ async function getSignedTxs(wallet, greeterContracts, gasPrice, gasLimit, chainI
const greeterContractAddress = randomIntFromInterval(0, greeterContracts.length - 1);
const greeterContract = new ethers.Contract(greeterContracts[greeterContractAddress], Greeter.abi, wallet);
const msg = `Greetings from Automated Test Number ${i}, Hello!`;
const trx = await greeterContract.populateTransaction.setGreeting(msg);
const trx = await greeterContract['setGreeting'].populateTransaction(msg);
trx.gasLimit = gasLimit;
trx.chainId = chainId;
trx.gasPrice = gasPrice;
Expand All @@ -59,13 +79,13 @@ async function getSignedTxs(wallet, greeterContracts, gasPrice, gasLimit, chainI
}

(async () => {
const provider = new ethers.providers.JsonRpcProvider(process.env.RELAY_BASE_URL);
const provider = new LoggingProvider(process.env.RELAY_BASE_URL);
const mainPrivateKeyString = process.env.PRIVATE_KEY;
const mainWallet = new ethers.Wallet(mainPrivateKeyString, new LoggingProvider(process.env.RELAY_BASE_URL));
const mainWallet = new ethers.Wallet(mainPrivateKeyString, provider);
console.log('RPC Server: ' + process.env.RELAY_BASE_URL);
console.log('Main Wallet Address: ' + mainWallet.address);
console.log(
'Main Wallet Initial Balance: ' + ethers.utils.formatEther(await provider.getBalance(mainWallet.address)) + ' HBAR',
'Main Wallet Initial Balance: ' + formatEther(await provider.getBalance(mainWallet.address)) + ' HBAR',
);
const usersCount = process.env.WALLETS_AMOUNT ? process.env.WALLETS_AMOUNT : 1;
const contractsCount = process.env.SMART_CONTRACTS_AMOUNT ? process.env.SMART_CONTRACTS_AMOUNT : 1;
Expand All @@ -74,20 +94,19 @@ async function getSignedTxs(wallet, greeterContracts, gasPrice, gasLimit, chainI
const contractFactory = new ethers.ContractFactory(Greeter.abi, Greeter.bytecode, mainWallet);
console.log(`Deploying Greeter SC ${i}`);
const contract = await contractFactory.deploy('Hey World!');
const contractAddress = contract.address;
await contract.waitForDeployment();
const contractAddress = contract.target;
console.log(`Greeter SC Address: ${contractAddress}`);
smartContracts.push(contractAddress);
}

const wallets = [];

const chainId = await mainWallet.getChainId();
const chainId = (await provider.getNetwork()).chainId;
const msgForEstimate = `Greetings from Automated Test Number i, Hello!`;
const contractForEstimate = new ethers.Contract(smartContracts[0], Greeter.abi, mainWallet);
const gasLimit = ethers.utils.hexValue(
Math.round((await contractForEstimate.estimateGas.setGreeting(msgForEstimate)) * 1.5),
); // extra
const gasPrice = ethers.utils.hexValue(Math.round((await mainWallet.getGasPrice()) * 1.5)); // with extra
const gasLimit = await contractForEstimate['setGreeting'].estimateGas(msgForEstimate);
const gasPrice = (await provider.getFeeData()).gasPrice;

for (let i = 0; i < usersCount; i++) {
const wallet = ethers.Wallet.createRandom();
Expand All @@ -102,7 +121,7 @@ async function getSignedTxs(wallet, greeterContracts, gasPrice, gasLimit, chainI
let tx = {
to: wallet.address,
// Convert currency unit from ether to wei
value: ethers.utils.parseEther(amountInEther),
value: parseEther(amountInEther),
};

// Send transaction
Expand All @@ -111,7 +130,7 @@ async function getSignedTxs(wallet, greeterContracts, gasPrice, gasLimit, chainI
});

const balance = await provider.getBalance(wallet.address);
console.log('balance: ', ethers.utils.formatEther(balance));
console.log('balance: ', formatEther(balance));

const walletProvider = new ethers.Wallet(wallet.privateKey, new LoggingProvider(process.env.RELAY_BASE_URL));
const signedTxCollection = await getSignedTxs(walletProvider, smartContracts, gasPrice, gasLimit, chainId);
Expand All @@ -120,8 +139,8 @@ async function getSignedTxs(wallet, greeterContracts, gasPrice, gasLimit, chainI
walletData['index'] = i;
walletData['address'] = wallet.address;
walletData['privateKey'] = wallet.privateKey;
walletData['latestBalance'] = ethers.utils.formatEther(balance);
walletData['latestNonce'] = await walletProvider.getTransactionCount();
walletData['latestBalance'] = formatEther(balance);
walletData['latestNonce'] = await walletProvider.getNonce();
walletData['signedTxs'] = signedTxCollection;
wallets.push(walletData);
}
Expand Down
2 changes: 1 addition & 1 deletion k6/src/scenarios/apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.1/index.js';

import { markdownReport } from '../lib/common.js';
import { funcs, options, scenarioDurationGauge } from './test/index.js';
import { setupTestParameters } from './test/bootstrapEnvParameters.js';
import { setupTestParameters } from '../lib/bootstrapEnvParameters.js';

function handleSummary(data) {
return {
Expand Down
95 changes: 95 additions & 0 deletions k6/src/scenarios/test-ws/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*-
*
* Hedera JSON RPC Relay
*
* Copyright (C) 2022-2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import ws from 'k6/ws';
import { check } from 'k6';

const errorField = 'error';
const resultField = 'result';

const isDebugMode = __ENV['DEBUG_MODE'] === 'true';

let requestId = 1;

function getPayLoad(methodName, paramInput = []) {
return JSON.stringify({
id: requestId++,
jsonrpc: '2.0',
method: methodName,
params: paramInput,
});
}

function connectToWebSocket(url, methodName, params = [], responseChecks = {}) {
return ws.connect(url, {}, (socket) => {
socket.on('open', () => {
const message = getPayLoad(methodName, params);
if (isDebugMode) {
console.log('Connected, sending request: ' + message);
}
socket.send(message);

socket.on('message', (message) => {
check(message, responseChecks);
socket.close();
});
});

socket.on('close', function () {
if (isDebugMode) {
console.log('Disconnected');
}
});

socket.on('error', (e) => {
if (isDebugMode) {
console.error('Received WebSocketError:', e);
}
});
});
}

function isNonErrorResponse(message) {
try {
const response = JSON.parse(message);
const success = response.hasOwnProperty(resultField) && !response.hasOwnProperty(errorField);
if (isDebugMode) {
console.log(`isNonErrorResponse: message=${message}, result=${success}`);
}
return success;
} catch (e) {
return false;
}
}

function isErrorResponse(message) {
try {
const response = JSON.parse(message);
const success = response.hasOwnProperty(errorField) && !response.hasOwnProperty(resultField);
if (isDebugMode) {
console.log(`isErrorResponse: message=${message}, result=${success}`);
}
return success;
} catch (e) {
return false;
}
}

export { connectToWebSocket, isNonErrorResponse, isErrorResponse };
37 changes: 37 additions & 0 deletions k6/src/scenarios/test-ws/eth_chainId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*-
*
* Hedera JSON RPC Relay
*
* Copyright (C) 2022-2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import { TestScenarioBuilder } from '../../lib/common.js';
import { connectToWebSocket, isNonErrorResponse } from './common.js';

const url = __ENV.WS_RELAY_BASE_URL;
const methodName = 'eth_chainId';

const { options, run } = new TestScenarioBuilder()
.name(methodName) // use unique scenario name among all tests
.request(() => connectToWebSocket(
url,
methodName,
[],
{ methodName: (r) => isNonErrorResponse(r) }
))
.build();

export { options, run };
Loading

0 comments on commit fe0c21f

Please sign in to comment.