Files
tendermint/scripts/qa/reporting/latency_throughput.py
Sergio Mena b06e1cea54 QA Process report for v0.37.x (and baseline for v0.34.x) (#9499)
* 1st version. 200 nodes. Missing rotating node

* Small fixes

* Addressed @jmalicevic's comment

* Explain in method how to set the tmint version to test. Improve result section

* 1st version of how to run the 'rotating node' testnet

* Apply suggestions from @williambanfield

Co-authored-by: William Banfield <4561443+williambanfield@users.noreply.github.com>

* Addressed @williambanfield's comments

* Added reference to Unix load metric

* Added total TXs

* Fixed some 'png's that got swapped. Excluded '.*-node-exporter' processes from memory plots

* Report for rotating node

* Adressed remaining comments from @williambanfield

* Cosmetic

* Addressed some of @thanethomson's comments

* Re-executed the 200 node tests and updated the corresponding sections of the report

* Ignore Python virtualenv directories

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Add latency vs throughput script

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Add README for latency vs throughput script

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Fix local links to folders

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* v034: only have one level-1 heading

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Adjust headings

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* v0.37.x: add links to issues/PRs

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* v0.37.x: add note about bug being present in v0.34

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* method: adjust heading depths

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Show data points on latency vs throughput plot

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Add latency vs throughput plots

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Correct mentioning of v0.34.21 and add heading

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Refactor latency vs throughput script

Update the latency vs throughput script to rather generate plots from
the "raw" CSV output from the loadtime reporting tool as opposed to the
separated CSV files from the experimental method.

Also update the relevant documentation, and regenerate the images from
the raw CSV data (resulting in pretty much the same plots as the
previous ones).

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Remove unused default duration const

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Adjust experiment start time to be more accurate and re-plot latency vs throughput

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Addressed @williambanfield's comments

* Apply suggestions from code review

Co-authored-by: William Banfield <4561443+williambanfield@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: William Banfield <4561443+williambanfield@users.noreply.github.com>

* scripts: Update latency vs throughput readme for clarity

Signed-off-by: Thane Thomson <connect@thanethomson.com>

Signed-off-by: Thane Thomson <connect@thanethomson.com>
Co-authored-by: William Banfield <4561443+williambanfield@users.noreply.github.com>
Co-authored-by: Thane Thomson <connect@thanethomson.com>
2022-10-17 22:08:51 +02:00

171 lines
6.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""
A simple script to parse the CSV output from the loadtime reporting tool (see
https://github.com/tendermint/tendermint/tree/main/test/loadtime/cmd/report).
Produces a plot of average transaction latency vs total transaction throughput
according to the number of load testing tool WebSocket connections to the
Tendermint node.
"""
import argparse
import csv
import logging
import sys
import matplotlib.pyplot as plt
import numpy as np
DEFAULT_TITLE = "Tendermint latency vs throughput"
def main():
parser = argparse.ArgumentParser(
description="Renders a latency vs throughput diagram "
"for a set of transactions provided by the loadtime reporting tool",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-t',
'--title',
default=DEFAULT_TITLE,
help='Plot title')
parser.add_argument('output_image',
help='Output image file (in PNG format)')
parser.add_argument(
'input_csv_file',
nargs='+',
help="CSV input file from which to read transaction data "
"- must have been generated by the loadtime reporting tool")
args = parser.parse_args()
logging.basicConfig(format='%(levelname)s\t%(message)s',
stream=sys.stdout,
level=logging.INFO)
plot_latency_vs_throughput(args.input_csv_file,
args.output_image,
title=args.title)
def plot_latency_vs_throughput(input_files, output_image, title=DEFAULT_TITLE):
avg_latencies, throughput_rates = process_input_files(input_files, )
fig, ax = plt.subplots()
connections = sorted(avg_latencies.keys())
for c in connections:
tr = np.array(throughput_rates[c])
al = np.array(avg_latencies[c])
label = '%d connection%s' % (c, '' if c == 1 else 's')
ax.plot(tr, al, 'o-', label=label)
ax.set_title(title)
ax.set_xlabel('Throughput rate (tx/s)')
ax.set_ylabel('Average transaction latency (s)')
plt.legend(loc='upper left')
plt.savefig(output_image)
def process_input_files(input_files):
# Experimental data from which we will derive the latency vs throughput
# statistics
experiments = {}
for input_file in input_files:
logging.info('Reading %s...' % input_file)
with open(input_file, 'rt') as inf:
reader = csv.DictReader(inf)
for tx in reader:
experiments = process_tx(experiments, tx)
return compute_experiments_stats(experiments)
def process_tx(experiments, tx):
exp_id = tx['experiment_id']
# Block time is nanoseconds from the epoch - convert to seconds
block_time = float(tx['block_time']) / (10**9)
# Duration is also in nanoseconds - convert to seconds
duration = float(tx['duration_ns']) / (10**9)
connections = int(tx['connections'])
rate = int(tx['rate'])
if exp_id not in experiments:
experiments[exp_id] = {
'connections': connections,
'rate': rate,
'block_time_min': block_time,
# We keep track of the latency associated with the minimum block
# time to estimate the start time of the experiment
'block_time_min_duration': duration,
'block_time_max': block_time,
'total_latencies': duration,
'tx_count': 1,
}
logging.info('Found experiment %s with rate=%d, connections=%d' %
(exp_id, rate, connections))
else:
# Validation
for field in ['connections', 'rate']:
val = int(tx[field])
if val != experiments[exp_id][field]:
raise Exception(
'Found multiple distinct values for field '
'"%s" for the same experiment (%s): %d and %d' %
(field, exp_id, val, experiments[exp_id][field]))
if block_time < experiments[exp_id]['block_time_min']:
experiments[exp_id]['block_time_min'] = block_time
experiments[exp_id]['block_time_min_duration'] = duration
if block_time > experiments[exp_id]['block_time_max']:
experiments[exp_id]['block_time_max'] = block_time
experiments[exp_id]['total_latencies'] += duration
experiments[exp_id]['tx_count'] += 1
return experiments
def compute_experiments_stats(experiments):
"""Compute average latency vs throughput rate statistics from the given
experiments"""
stats = {}
# Compute average latency and throughput rate for each experiment
for exp_id, exp in experiments.items():
conns = exp['connections']
avg_latency = exp['total_latencies'] / exp['tx_count']
exp_start_time = exp['block_time_min'] - exp['block_time_min_duration']
exp_duration = exp['block_time_max'] - exp_start_time
throughput_rate = exp['tx_count'] / exp_duration
if conns not in stats:
stats[conns] = []
stats[conns].append({
'avg_latency': avg_latency,
'throughput_rate': throughput_rate,
})
# Sort stats for each number of connections in order of increasing
# throughput rate, and then extract average latencies and throughput rates
# as separate data series.
conns = sorted(stats.keys())
avg_latencies = {}
throughput_rates = {}
for c in conns:
stats[c] = sorted(stats[c], key=lambda s: s['throughput_rate'])
avg_latencies[c] = []
throughput_rates[c] = []
for s in stats[c]:
avg_latencies[c].append(s['avg_latency'])
throughput_rates[c].append(s['throughput_rate'])
logging.info('For %d connection(s): '
'throughput rate = %.6f tx/s\t'
'average latency = %.6fs' %
(c, s['throughput_rate'], s['avg_latency']))
return (avg_latencies, throughput_rates)
if __name__ == "__main__":
main()