Skip to content

Commit

Permalink
[Testing] Add load-testing foundation (#278)
Browse files Browse the repository at this point in the history
* load testing foundation

* --wip-- [skip ci]

* --wip-- [skip ci]

* --wip-- [skip ci]

* --wip-- [skip ci]

* add docs and better structure

* wip

* requested changes

* change error output

* more cleanup

* requested changes
  • Loading branch information
okdas authored Jan 19, 2024
1 parent b02403b commit 98cb3e8
Show file tree
Hide file tree
Showing 12 changed files with 317 additions and 583 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ go.work.sum
# TODO_TECHDEBT: It seems that .dot files come and go so we need to figure out the root cause: https://github.com/pokt-network/poktroll/pull/177/files#r1392521547
# **/*.dot

# Load-test summary report, mentioning in gitignore to avoid accidentally committing it
summary.html

# Quickstart helpers
shannon_appgate_config.yaml
shannon_app_config.yaml
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ go_develop: proto_regen go_mockgen ## Generate protos and mocks
.PHONY: go_develop_and_test
go_develop_and_test: go_develop go_test ## Generate protos, mocks and run all tests

.PHONY: load_test_simple
load_test_simple: ## Runs the simpliest load test through the whole stack (appgate -> relayminer -> anvil)
k6 run load-testing/tests/appGateServerEtherium.js

#############
### TODOS ###
#############
Expand Down
2 changes: 0 additions & 2 deletions docs/static/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79117,8 +79117,6 @@ definitions:
description: params holds all the parameters of this module.
type: object
description: QueryParamsResponse is response type for the Query/Params RPC method.
poktroll.service.MsgAddServiceResponse:
type: object
poktroll.tokenomics.Params:
type: object
properties:
Expand Down
136 changes: 136 additions & 0 deletions docusaurus/docs/infrastructure/testing/load_testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
sidebar_position: 1
title: Load Testing
---

# Load Testing <!-- omit in toc -->

Poktroll load-testing suite.

- [Overview](#overview)
- [Dependencies](#dependencies)
- [How to run tests](#how-to-run-tests)
- [Common k6 CLI flags](#common-k6-cli-flags)
- [Understanding Output](#understanding-output)
- [CLI Output](#cli-output)
- [HTML Report](#html-report)
- [Adding a new load test](#adding-a-new-load-test)
- [Sophisticated Reporting](#sophisticated-reporting)
- [Troubleshooting](#troubleshooting)
- [File structure](#file-structure)

## Overview

We use [k6](https://k6.io/) for load testing. For detailed information about k6 internals and best practices, please refer to the [k6 documentation](https://grafana.com/docs/k6/latest/).

## Dependencies

- [k6](https://grafana.com/docs/k6/latest/get-started/installation/)
- (For local suite execution) [LocalNet](../localnet.md)

## How to run tests

Tests are stored in the [load-testing/tests](https://github.com/pokt-network/poktroll/tree/main/load-testing/tests) folder, covering various use cases.

For instance, here's a basic load test sending requests through `AppGateServer`, which proxies them to `RelayMiner`:

```bash
k6 run load-testing/tests/appGateServerEtherium.js
```

### Common k6 CLI flags

The `k6` load testing tool provides various command-line flags to customize and control your load tests. Below are some of the key flags that you can use:

- `--vus`: Specifies the number of virtual users (VUs) to simulate. This flag allows you to set the concurrency level of your load test.
- Example: `k6 run script.js --vus=50` runs the test with 50 virtual users.

- `--duration`: Defines the duration for which the test should run. You can specify the time in seconds (s), minutes (m), or hours (h).
- Example: `k6 run script.js --duration=1m` runs the test for 1 minute.

- `--http-debug`: Enables HTTP debugging. This flag can be set to `full` for detailed logging of all HTTP requests and responses. Useful for troubleshooting and debugging your tests.
- Use `--http-debug` for a summary of HTTP requests.
- Use `--http-debug=full` for detailed request and response logging.
- Example: `k6 run script.js --http-debug=full` provides a detailed log of HTTP transactions.

The default configurations for VUs and duration are in the [config file](https://github.com/pokt-network/poktroll/tree/main/load-testing/config/index.js). Override these defaults by passing the appropriate flags to the `k6` command. For example:

```bash
k6 run load-testing/tests/appGateServerEtherium.js --vus=300 --duration=30s
```

For a comprehensive list of available flags and their usage, refer to the [k6 documentation](https://grafana.com/docs/k6/latest/).

## Understanding Output

### CLI Output

Post-test, basic performance metrics are available:

```
scenarios: (100.00%) 1 scenario, 100 max VUs, 1m30s max duration (incl. graceful stop):
* default: 100 looping VUs for 1m0s (gracefulStop: 30s)
INFO[0061] [k6-reporter v2.3.0] Generating HTML summary report source=console
✓ is status 200
✓ is successful JSON-RPC response
checks.........................: 100.00% ✓ 12000 ✗ 0
data_received..................: 1.6 MB 27 kB/s
data_sent......................: 1.2 MB 19 kB/s
http_req_blocked...............: avg=52.52µs min=0s med=2µs max=7.08ms p(90)=4µs p(95)=6µs
http_req_connecting............: avg=37.73µs min=0s med=0s max=5.48ms p(90)=0s p(95)=0s
http_req_duration..............: avg=8.51ms min=1.58ms med=5.98ms max=81.98ms p(90)=15.45ms p(95)=19.97ms
{ expected_response:true }...: avg=8.51ms min=1.58ms med=5.98ms max=81.98ms p(90)=15.45ms p(95)=19.97ms
http_req_failed................: 0.00% ✓ 0 ✗ 6000
http_req_receiving.............: avg=42.18µs min=5µs med=25µs max=5.71ms p(90)=65µs p(95)=125µs
http_req_sending...............: avg=26.5µs min=2µs med=9µs max=7.2ms p(90)=21µs p(95)=42µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=8.44ms min=1.55ms med=5.93ms max=81.28ms p(90)=15.38ms p(95)=19.82ms
http_reqs......................: 6000 98.915624/s
iteration_duration.............: avg=1s min=1s med=1s max=1.08s p(90)=1.01s p(95)=1.02s
iterations.....................: 6000 98.915624/s
vus............................: 100 min=100 max=100
running (1m00.7s), 000/100 VUs, 6000 complete and 0 interrupted iterations
default ✓ [======================================] 100 VUs 1m0s
```

### HTML Report

An HTML report is generated in the execution directory. Open it in your default browser:

```
open summary.html
```

### Adding a new load test

TODO(@okdas): Add link to PR next time a new type of load test is added.


### Sophisticated Reporting

We're developing advanced reporting that integrates additional tags set in the code. This will require time-series databases and is planned for DevNets.

## Troubleshooting

To debug, activate the logging feature:

`--http-debug` or `--http-debug=full`

## File structure

```
load-testing
├── README.md
├── config
│ └── index.js
├── modules # reusable code for scenarios and tests
│ └── etheriumRequests.js
├── scenarios # different scenarios for tests
│ └── requestBlockNumberEtherium.js
└── tests # test scripts
├── anvilDirectEtherium.js
└── appGateServerEtherium.js
```
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
sidebar_position: 2
title: Load Test 1 - Plan
title: Load Testing - Plan
---

# Load Test #1 - Plan <!-- omit in toc -->
Expand Down
Empty file added load-testing/README.md
Empty file.
66 changes: 66 additions & 0 deletions load-testing/config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";
import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.1/index.js";

// Environment configuration
export const ENV_CONFIG = {
anvilBaseUrl: __ENV.ANVIL_BASE_URL || 'http://localhost:8547',
nginxBaseUrl: __ENV.NGINX_BASE_URL || 'http://localhost:8548',
AppGateServerAnvilUrl: __ENV.APP_GATE_SERVER_ANVIL_URL || 'http://localhost:42069/anvil',
};

// We can export this function in our tests to generate HTML summary.
export function handleSummary(data) {
return {
"summary.html": htmlReport(data),
stdout: textSummary(data, { indent: " ", enableColors: true }),
};
}

export const options = {
// A number specifying the number of VUs to run concurrently.
vus: 100,
// A string specifying the total duration of the test run.
duration: '60s',

// The following section contains configuration options for execution of this
// test script in Grafana Cloud.
//
// See https://grafana.com/docs/grafana-cloud/k6/get-started/run-cloud-tests-from-the-cli/
// to learn about authoring and running k6 test scripts in Grafana k6 Cloud.
//
// ext: {
// loadimpact: {
// // The ID of the project to which the test is assigned in the k6 Cloud UI.
// // By default tests are executed in default project.
// projectID: "",
// // The name of the test in the k6 Cloud UI.
// // Test runs with the same name will be grouped.
// name: "script.js"
// }
// },

// Uncomment this section to enable the use of Browser API in your tests.
//
// See https://grafana.com/docs/k6/latest/using-k6-browser/running-browser-tests/ to learn more
// about using Browser API in your test scripts.
//
// scenarios: {
// // The scenario name appears in the result summary, tags, and so on.
// // You can give the scenario any name, as long as each name in the script is unique.
// ui: {
// // Executor is a mandatory parameter for browser-based tests.
// // Shared iterations in this case tells k6 to reuse VUs to execute iterations.
// //
// // See https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/ for other executor types.
// executor: 'shared-iterations',
// options: {
// browser: {
// // This is a mandatory parameter that instructs k6 to launch and
// // connect to a chromium-based browser, and use it to run UI-based
// // tests.
// type: 'chromium',
// },
// },
// },
// }
};
39 changes: 39 additions & 0 deletions load-testing/modules/sendJsonRPCRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Import necessary modules
import http from 'k6/http';
import { check } from 'k6';

// Function to send a generic JSON-RPC request to Anvil
export function sendJsonRPCRequest(baseUrl, method, params = [], tags = {}) {
const payload = JSON.stringify({
jsonrpc: "2.0",
method: method,
params: params,
id: 1
});

const requestOptions = {
headers: {
"Content-Type": "application/json",
},
tags: tags
};

let response = http.post(baseUrl, payload, requestOptions);

// Basic check for HTTP 200 OK and a valid JSON-RPC response
let passed = check(response, {
"is status 200": (r) => r.status === 200,
"is successful JSON-RPC response": (r) => {
let jsonResponse = JSON.parse(r.body);
// Check for 'result' and 'id', and ensure 'error' is not present
return jsonResponse.hasOwnProperty("result") && jsonResponse.hasOwnProperty("id") && !jsonResponse.hasOwnProperty("error");
}
}, tags);

if (!passed) {
// Logging output includes vital information for troubleshooting: request/response body, status code, etc.
console.log(`Request to ${response.request.url} failed:`, JSON.stringify(response, null, 2));
}

return response;
}
26 changes: 26 additions & 0 deletions load-testing/scenarios/requestBlockNumberEtherium.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Import necessary modules
import { check } from 'k6';
import { sendJsonRPCRequest } from '../modules/sendJsonRPCRequest.js';

export function requestBlockNumberEtheriumScenario(baseUrl) {
// Example method and parameters for the Ethereum JSON-RPC request
const method = "eth_blockNumber";
const params = [];

// We can populate tags in addition to the defaults.
// const tags = {
// method: method,
// baseUrl: baseUrl,
// }

// Send request and receive response
let response = sendJsonRPCRequest(baseUrl, method, params);

// Additional checks specific to the scenario can be written like that:
// check(response, {
// "is the response format correct": (r) => {
// let jsonResponse = JSON.parse(r.body);
// return jsonResponse.hasOwnProperty("result") && jsonResponse.hasOwnProperty("id");
// },
// }, tags);
}
17 changes: 17 additions & 0 deletions load-testing/tests/anvil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Our custom handleSummary produces HTML report with CLI output and some default configuration.
export { handleSummary, options } from '../config/index.js';
import { requestBlockNumberEtheriumScenario } from '../scenarios/requestBlockNumberEtherium.js';
import { ENV_CONFIG } from '../config/index.js';
import { sleep } from 'k6';


// The function that defines VU logic.
//
// See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more
// about authoring k6 scripts.
export default function () {
requestBlockNumberEtheriumScenario(ENV_CONFIG.anvilBaseUrl);

// Simulate think time
sleep(1);
}
25 changes: 25 additions & 0 deletions load-testing/tests/appGateServerEtherium.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Our custom handleSummary produces HTML report with CLI output and some default configuration.
export { handleSummary, options } from '../config/index.js';
import { requestBlockNumberEtheriumScenario } from '../scenarios/requestBlockNumberEtherium.js';
import { ENV_CONFIG } from '../config/index.js';
import { sleep } from 'k6';

// TODO(@okdas): expand options to allow multiple stages:
// export const options = {
// stages: [
// { duration: '30s', target: 20 },
// { duration: '1m30s', target: 10 },
// { duration: '20s', target: 0 },
// ],
// };

// The function that defines VU logic.
//
// See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more
// about authoring k6 scripts.
export default function () {
requestBlockNumberEtheriumScenario(ENV_CONFIG.AppGateServerAnvilUrl);

// Simulate think time
sleep(1);
}
Loading

0 comments on commit 98cb3e8

Please sign in to comment.