github.com/vipernet-xyz/tm@v0.34.24/scripts/qa/reporting/latency_throughput.py (about)

     1  #!/usr/bin/env python3
     2  """
     3  A simple script to parse the CSV output from the loadtime reporting tool (see
     4  https://github.com/vipernet-xyz/tm/tree/main/test/loadtime/cmd/report).
     5  
     6  Produces a plot of average transaction latency vs total transaction throughput
     7  according to the number of load testing tool WebSocket connections to the
     8  Tendermint node.
     9  """
    10  
    11  import argparse
    12  import csv
    13  import logging
    14  import sys
    15  import matplotlib.pyplot as plt
    16  import numpy as np
    17  
    18  DEFAULT_TITLE = "Tendermint latency vs throughput"
    19  
    20  
    21  def main():
    22      parser = argparse.ArgumentParser(
    23          description="Renders a latency vs throughput diagram "
    24          "for a set of transactions provided by the loadtime reporting tool",
    25          formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    26      parser.add_argument('-t',
    27                          '--title',
    28                          default=DEFAULT_TITLE,
    29                          help='Plot title')
    30      parser.add_argument('output_image',
    31                          help='Output image file (in PNG format)')
    32      parser.add_argument(
    33          'input_csv_file',
    34          nargs='+',
    35          help="CSV input file from which to read transaction data "
    36          "- must have been generated by the loadtime reporting tool")
    37      args = parser.parse_args()
    38  
    39      logging.basicConfig(format='%(levelname)s\t%(message)s',
    40                          stream=sys.stdout,
    41                          level=logging.INFO)
    42      plot_latency_vs_throughput(args.input_csv_file,
    43                                 args.output_image,
    44                                 title=args.title)
    45  
    46  
    47  def plot_latency_vs_throughput(input_files, output_image, title=DEFAULT_TITLE):
    48      avg_latencies, throughput_rates = process_input_files(input_files, )
    49  
    50      fig, ax = plt.subplots()
    51  
    52      connections = sorted(avg_latencies.keys())
    53      for c in connections:
    54          tr = np.array(throughput_rates[c])
    55          al = np.array(avg_latencies[c])
    56          label = '%d connection%s' % (c, '' if c == 1 else 's')
    57          ax.plot(tr, al, 'o-', label=label)
    58  
    59      ax.set_title(title)
    60      ax.set_xlabel('Throughput rate (tx/s)')
    61      ax.set_ylabel('Average transaction latency (s)')
    62  
    63      plt.legend(loc='upper left')
    64      plt.savefig(output_image)
    65  
    66  
    67  def process_input_files(input_files):
    68      # Experimental data from which we will derive the latency vs throughput
    69      # statistics
    70      experiments = {}
    71  
    72      for input_file in input_files:
    73          logging.info('Reading %s...' % input_file)
    74  
    75          with open(input_file, 'rt') as inf:
    76              reader = csv.DictReader(inf)
    77              for tx in reader:
    78                  experiments = process_tx(experiments, tx)
    79  
    80      return compute_experiments_stats(experiments)
    81  
    82  
    83  def process_tx(experiments, tx):
    84      exp_id = tx['experiment_id']
    85      # Block time is nanoseconds from the epoch - convert to seconds
    86      block_time = float(tx['block_time']) / (10**9)
    87      # Duration is also in nanoseconds - convert to seconds
    88      duration = float(tx['duration_ns']) / (10**9)
    89      connections = int(tx['connections'])
    90      rate = int(tx['rate'])
    91  
    92      if exp_id not in experiments:
    93          experiments[exp_id] = {
    94              'connections': connections,
    95              'rate': rate,
    96              'block_time_min': block_time,
    97              # We keep track of the latency associated with the minimum block
    98              # time to estimate the start time of the experiment
    99              'block_time_min_duration': duration,
   100              'block_time_max': block_time,
   101              'total_latencies': duration,
   102              'tx_count': 1,
   103          }
   104          logging.info('Found experiment %s with rate=%d, connections=%d' %
   105                       (exp_id, rate, connections))
   106      else:
   107          # Validation
   108          for field in ['connections', 'rate']:
   109              val = int(tx[field])
   110              if val != experiments[exp_id][field]:
   111                  raise Exception(
   112                      'Found multiple distinct values for field '
   113                      '"%s" for the same experiment (%s): %d and %d' %
   114                      (field, exp_id, val, experiments[exp_id][field]))
   115  
   116          if block_time < experiments[exp_id]['block_time_min']:
   117              experiments[exp_id]['block_time_min'] = block_time
   118              experiments[exp_id]['block_time_min_duration'] = duration
   119          if block_time > experiments[exp_id]['block_time_max']:
   120              experiments[exp_id]['block_time_max'] = block_time
   121  
   122          experiments[exp_id]['total_latencies'] += duration
   123          experiments[exp_id]['tx_count'] += 1
   124  
   125      return experiments
   126  
   127  
   128  def compute_experiments_stats(experiments):
   129      """Compute average latency vs throughput rate statistics from the given
   130      experiments"""
   131      stats = {}
   132  
   133      # Compute average latency and throughput rate for each experiment
   134      for exp_id, exp in experiments.items():
   135          conns = exp['connections']
   136          avg_latency = exp['total_latencies'] / exp['tx_count']
   137          exp_start_time = exp['block_time_min'] - exp['block_time_min_duration']
   138          exp_duration = exp['block_time_max'] - exp_start_time
   139          throughput_rate = exp['tx_count'] / exp_duration
   140          if conns not in stats:
   141              stats[conns] = []
   142  
   143          stats[conns].append({
   144              'avg_latency': avg_latency,
   145              'throughput_rate': throughput_rate,
   146          })
   147  
   148      # Sort stats for each number of connections in order of increasing
   149      # throughput rate, and then extract average latencies and throughput rates
   150      # as separate data series.
   151      conns = sorted(stats.keys())
   152      avg_latencies = {}
   153      throughput_rates = {}
   154      for c in conns:
   155          stats[c] = sorted(stats[c], key=lambda s: s['throughput_rate'])
   156          avg_latencies[c] = []
   157          throughput_rates[c] = []
   158          for s in stats[c]:
   159              avg_latencies[c].append(s['avg_latency'])
   160              throughput_rates[c].append(s['throughput_rate'])
   161              logging.info('For %d connection(s): '
   162                           'throughput rate = %.6f tx/s\t'
   163                           'average latency = %.6fs' %
   164                           (c, s['throughput_rate'], s['avg_latency']))
   165  
   166      return (avg_latencies, throughput_rates)
   167  
   168  
   169  if __name__ == "__main__":
   170      main()