| 1 | #!/usr/bin/python3
 | 
|---|
| 2 | 
 | 
|---|
| 3 | import argparse
 | 
|---|
| 4 | import decimal
 | 
|---|
| 5 | import math
 | 
|---|
| 6 | import re
 | 
|---|
| 7 | import sys
 | 
|---|
| 8 | 
 | 
|---|
| 9 | import collections, functools, operator
 | 
|---|
| 10 | 
 | 
|---|
| 11 | def parse(reg, lines):
 | 
|---|
| 12 |         m = [re.findall(reg,l) for l in lines]
 | 
|---|
| 13 |         return [*filter(None, m)][0][0]
 | 
|---|
| 14 | 
 | 
|---|
| 15 | def wavg(vals, ws):
 | 
|---|
| 16 |         t = sum(ws)
 | 
|---|
| 17 |         if t == 0:
 | 
|---|
| 18 |                 return 0.0
 | 
|---|
| 19 |         s = sum([vals[i] * ws[i] for i in range(len(vals))])
 | 
|---|
| 20 |         return s / t
 | 
|---|
| 21 | 
 | 
|---|
| 22 | def hist(s):
 | 
|---|
| 23 |         s = s.split()
 | 
|---|
| 24 |         h = [int(v) for v in s]
 | 
|---|
| 25 |         return dict([(k, v) for (k,v) in enumerate(h) if v != 0])
 | 
|---|
| 26 | 
 | 
|---|
| 27 | class Result:
 | 
|---|
| 28 |         def __init__(self):
 | 
|---|
| 29 |                 self.total = {}
 | 
|---|
| 30 |                 self.connect = {}
 | 
|---|
| 31 |                 self.request = {}
 | 
|---|
| 32 |                 self.reply = {}
 | 
|---|
| 33 |                 self.misc = {}
 | 
|---|
| 34 |                 self.errors = {}
 | 
|---|
| 35 |                 self.session = {}
 | 
|---|
| 36 | 
 | 
|---|
| 37 |         @staticmethod
 | 
|---|
| 38 |         def from_file(file):
 | 
|---|
| 39 |                 r = Result()
 | 
|---|
| 40 |                 lines  = [l for l in file]
 | 
|---|
| 41 |                 print(lines[0].strip())
 | 
|---|
| 42 |                 #------------------------------
 | 
|---|
| 43 |                 # total
 | 
|---|
| 44 |                 totals = parse(r'^Total: connections ([0-9]+) requests ([0-9]+) replies ([0-9]+) test-duration ([\.0-9]+) s', lines)
 | 
|---|
| 45 |                 r.total = {
 | 
|---|
| 46 |                         'connections': int(totals[0]),
 | 
|---|
| 47 |                         'requests': int(totals[1]),
 | 
|---|
| 48 |                         'replies': int(totals[2]),
 | 
|---|
| 49 |                         'duration': float(totals[3])
 | 
|---|
| 50 |                 }
 | 
|---|
| 51 | 
 | 
|---|
| 52 |                 #------------------------------
 | 
|---|
| 53 |                 # connection
 | 
|---|
| 54 |                 connection1 = parse(r'^Connection rate: ([\.0-9]+) conn/s \(([\.0-9]+) ms/conn, <=([0-9]+) concurrent connections\)', lines)
 | 
|---|
| 55 |                 connection2 = parse(r'^Connection time \[ms\]: min ([\.0-9]+) avg ([\.0-9]+) max ([\.0-9]+) median ([\.0-9]+) stddev ([\.0-9]+)', lines)
 | 
|---|
| 56 |                 connection3 = parse(r'^Connection time \[ms\]: connect ([\.0-9]+)', lines)
 | 
|---|
| 57 |                 connection4 = parse(r'^Connection length \[replies/conn\]: ([\.0-9]+)', lines)
 | 
|---|
| 58 |                 r.connect = {
 | 
|---|
| 59 |                         'rate': { 'conn/s': float(connection1[0]), 'ms/conn': float(connection1[1]), '<=': int(connection1[2]) },
 | 
|---|
| 60 |                         'time': { 'min': float(connection2[0]), 'avg': float(connection2[1]), 'max': float(connection2[2]), 'median': float(connection2[3]), 'stddev': float(connection2[4]) },
 | 
|---|
| 61 |                         'connect': float(connection3),
 | 
|---|
| 62 |                         'length': float(connection4)
 | 
|---|
| 63 |                 }
 | 
|---|
| 64 | 
 | 
|---|
| 65 |                 #------------------------------
 | 
|---|
| 66 |                 # request
 | 
|---|
| 67 |                 request1 = parse(r'^Request rate: ([\.0-9]+) req/s \(([\.0-9]+) ms/req\)', lines)
 | 
|---|
| 68 |                 request2 = parse(r'^Request size \[B\]: ([\.0-9]+)', lines)
 | 
|---|
| 69 |                 r.request = {
 | 
|---|
| 70 |                         'req/s': float(request1[0]),
 | 
|---|
| 71 |                         'ms/req': float(request1[1]),
 | 
|---|
| 72 |                         'size': float(request2)
 | 
|---|
| 73 |                 }
 | 
|---|
| 74 | 
 | 
|---|
| 75 |                 #------------------------------
 | 
|---|
| 76 |                 # reply
 | 
|---|
| 77 |                 replies1 = parse(r'^Reply rate \[replies/s\]: min ([\.0-9]+) avg ([\.0-9]+) max ([\.0-9]+) stddev ([\.0-9]+)', lines)
 | 
|---|
| 78 |                 replies2 = parse(r'^Reply time \[ms\]: response ([\.0-9]+) transfer ([\.0-9]+)', lines)
 | 
|---|
| 79 |                 replies3 = parse(r'^Reply size \[B\]: header ([\.0-9]+) content ([\.0-9]+) footer ([\.0-9]+) \(total ([\.0-9]+)\)', lines)
 | 
|---|
| 80 |                 replies4 = parse(r'^Reply status: 1xx=([0-9]+) 2xx=([0-9]+) 3xx=([0-9]+) 4xx=([0-9]+) 5xx=([0-9]+)', lines)
 | 
|---|
| 81 |                 r.reply = {
 | 
|---|
| 82 |                         'rate' : { 'min': float(replies1[0]), 'avg': float(replies1[1]), 'max': float(replies1[2]), 'stddev': float(replies1[3]) },
 | 
|---|
| 83 |                         'time' : { 'response': float(replies2[0]), 'transfer': float(replies2[1]) },
 | 
|---|
| 84 |                         'size' : { 'header': float(replies3[0]), 'content': float(replies3[1]), 'footer': float(replies3[2]), 'total': float(replies3[3]) },
 | 
|---|
| 85 |                         'status' : { '1xx': int(replies4[0]), '2xx': int(replies4[1]), '3xx': int(replies4[2]), '4xx': int(replies4[3]), '5xx': int(replies4[4]) }
 | 
|---|
| 86 |                 }
 | 
|---|
| 87 | 
 | 
|---|
| 88 |                 #------------------------------
 | 
|---|
| 89 |                 # misc
 | 
|---|
| 90 |                 misc1 = parse(r'^CPU time \[s\]: user ([\.0-9]+) system ([\.0-9]+) \(user ([\.0-9]+)% system ([\.0-9]+)% total ([\.0-9]+)%\)', lines)
 | 
|---|
| 91 |                 misc2 = parse(r'^Net I/O: ([\.0-9]+) KB/s \(([\.0-9]+)\*10\^([0-9]+) bps\)', lines)
 | 
|---|
| 92 |                 r.misc = {
 | 
|---|
| 93 |                         'usr': float(misc1[0]),
 | 
|---|
| 94 |                         'sys': float(misc1[1]),
 | 
|---|
| 95 |                         'usr%': float(misc1[2]),
 | 
|---|
| 96 |                         'sys%': float(misc1[3]),
 | 
|---|
| 97 |                         'total%': float(misc1[4]),
 | 
|---|
| 98 |                         'KB/S': float(misc2[0]),
 | 
|---|
| 99 |                         'bps': float(misc2[1]) * math.pow(10, int(misc2[2]))
 | 
|---|
| 100 |                 }
 | 
|---|
| 101 | 
 | 
|---|
| 102 |                 #------------------------------
 | 
|---|
| 103 |                 # errors
 | 
|---|
| 104 |                 errors1 = parse(r'^Errors: total ([0-9]+) client-timo ([0-9]+) socket-timo ([0-9]+) connrefused ([0-9]+) connreset ([0-9]+)', lines)
 | 
|---|
| 105 |                 errors2 = parse(r'^Errors: fd-unavail ([0-9]+) addrunavail ([0-9]+) ftab-full ([0-9]+) other ([0-9]+)', lines)
 | 
|---|
| 106 |                 r.errors = {
 | 
|---|
| 107 |                         'total': int(errors1[0]),
 | 
|---|
| 108 |                         'client-timout': int(errors1[1]),
 | 
|---|
| 109 |                         'socket-timout': int(errors1[2]),
 | 
|---|
| 110 |                         'connection-refused': int(errors1[3]),
 | 
|---|
| 111 |                         'connection-reset': int(errors1[4]),
 | 
|---|
| 112 |                         'fd-unavailable': int(errors2[0]),
 | 
|---|
| 113 |                         'address-unavailable': int(errors2[1]),
 | 
|---|
| 114 |                         'ftab-full': int(errors2[2]),
 | 
|---|
| 115 |                         'other': int(errors2[3])
 | 
|---|
| 116 |                 }
 | 
|---|
| 117 | 
 | 
|---|
| 118 |                 #------------------------------
 | 
|---|
| 119 |                 # session
 | 
|---|
| 120 |                 session1 = parse(r'^Session rate \[sess/s\]: min ([\.0-9]+) avg ([\.0-9]+) max ([\.0-9]+) stddev ([\.0-9]+) \(([0-9]+)/([0-9]+)\)', lines)
 | 
|---|
| 121 |                 session2 = parse(r'^Session: avg ([\.0-9]+) connections/session', lines)
 | 
|---|
| 122 |                 session3 = parse(r'^Session lifetime \[s\]: ([\.0-9]+)', lines)
 | 
|---|
| 123 |                 session4 = parse(r'^Session failtime \[s\]: ([\.0-9]+)', lines)
 | 
|---|
| 124 |                 session5 = parse(r'^Session length histogram: ([ 0-9]+)', lines)
 | 
|---|
| 125 |                 r.session = {
 | 
|---|
| 126 |                         'rate': { 'min': float(session1[0]), 'avg': float(session1[1]), 'max': float(session1[2]), 'stddev': float(session1[3]) },
 | 
|---|
| 127 |                         'successes': int(session1[4]),
 | 
|---|
| 128 |                         'totals': int(session1[5]),
 | 
|---|
| 129 |                         'conns/ses': float(session2),
 | 
|---|
| 130 |                         'lifetime': float(session3),
 | 
|---|
| 131 |                         'failtime': float(session4),
 | 
|---|
| 132 |                         'hist': hist(session5)
 | 
|---|
| 133 |                 }
 | 
|---|
| 134 | 
 | 
|---|
| 135 |                 return r
 | 
|---|
| 136 | 
 | 
|---|
| 137 | if __name__ == "__main__":
 | 
|---|
| 138 |         #------------------------------
 | 
|---|
| 139 |         # parse args
 | 
|---|
| 140 |         parser = argparse.ArgumentParser(description='Script aggregates httperf output')
 | 
|---|
| 141 |         parser.add_argument('files', metavar='files', type=argparse.FileType('r'), nargs='*', help='a list of files to aggregate')
 | 
|---|
| 142 | 
 | 
|---|
| 143 |         try:
 | 
|---|
| 144 |                 args =  parser.parse_args()
 | 
|---|
| 145 |         except:
 | 
|---|
| 146 |                 print('ERROR: invalid arguments', file=sys.stderr)
 | 
|---|
| 147 |                 parser.print_help(sys.stderr)
 | 
|---|
| 148 |                 sys.exit(1)
 | 
|---|
| 149 | 
 | 
|---|
| 150 |         if len(args.files) == 0:
 | 
|---|
| 151 |                 print('No input files', file=sys.stderr)
 | 
|---|
| 152 |                 parser.print_help(sys.stderr)
 | 
|---|
| 153 |                 sys.exit(1)
 | 
|---|
| 154 | 
 | 
|---|
| 155 |         #------------------------------
 | 
|---|
| 156 |         # Construct objects
 | 
|---|
| 157 |         results = [Result.from_file(f) for f in args.files]
 | 
|---|
| 158 | 
 | 
|---|
| 159 |         #==================================================
 | 
|---|
| 160 |         # Print
 | 
|---|
| 161 |         #==================================================
 | 
|---|
| 162 |         totals = dict(functools.reduce(operator.add, map(collections.Counter, [r.total for r in results])))
 | 
|---|
| 163 |         totals['duration-'] = min([r.total['duration'] for r in results])
 | 
|---|
| 164 |         totals['duration+'] = max([r.total['duration'] for r in results])
 | 
|---|
| 165 |         print("")
 | 
|---|
| 166 |         print("")
 | 
|---|
| 167 |         print("Total: connections {:,} requests {:,} replies {:,} test-duration {}-{} s".format(totals['connections'], totals['requests'], totals['replies'], totals['duration-'], totals['duration+']))
 | 
|---|
| 168 |         print("")
 | 
|---|
| 169 | 
 | 
|---|
| 170 |         #==================================================
 | 
|---|
| 171 |         connections = {
 | 
|---|
| 172 |                 'conn/s': sum([r.connect['rate']['conn/s'] for r in results]),
 | 
|---|
| 173 |                 '<=': sum([r.connect['rate']['<='] for r in results]),
 | 
|---|
| 174 |                 'min': min([r.connect['time']['min'] for r in results]),
 | 
|---|
| 175 |                 'avg': wavg([r.connect['time']['avg'] for r in results], [r.total['connections'] for r in results]),
 | 
|---|
| 176 |                 'max': max([r.connect['time']['max'] for r in results]),
 | 
|---|
| 177 |                 'median': wavg([r.connect['time']['median'] for r in results], [r.total['connections'] for r in results]),
 | 
|---|
| 178 |                 'stddev': wavg([r.connect['time']['stddev'] for r in results], [r.total['connections'] for r in results]),
 | 
|---|
| 179 |                 'connect': wavg([r.connect['connect'] for r in results], [r.total['connections'] for r in results]),
 | 
|---|
| 180 |                 'length': wavg([r.connect['length'] for r in results], [r.total['connections'] for r in results])
 | 
|---|
| 181 |         }
 | 
|---|
| 182 |         print("Connection rate: {:,.2f} conn/s ({:.2f} ms/conn, <={:,} concurrent connections)".format(connections['conn/s'], 1000.0 / connections['conn/s'], connections['<=']))
 | 
|---|
| 183 |         print("Connection time [ms]: min {:,.2f} avg {:,.2f} max {:,.2f} avg median {:,.2f} avg stddev {:,.2f}".format(connections['min'], connections['avg'], connections['max'], connections['median'], connections['stddev']))
 | 
|---|
| 184 |         print("Connection time [ms]: connect {:,.2f}".format(connections['connect']))
 | 
|---|
| 185 |         print("Connection length [replies/conn]: {:,.2f}".format(connections['length']))
 | 
|---|
| 186 |         print("")
 | 
|---|
| 187 | 
 | 
|---|
| 188 |         #==================================================
 | 
|---|
| 189 |         requests = {
 | 
|---|
| 190 |                 'req/s': sum([r.request['req/s'] for r in results]),
 | 
|---|
| 191 |                 'size': wavg([r.request['size'] for r in results], [r.total['requests'] for r in results])
 | 
|---|
| 192 |         }
 | 
|---|
| 193 |         print("Request rate: {:,.2f} req/s ({:.2f} ms/req)".format(requests['req/s'], 1000.0 / requests['req/s']))
 | 
|---|
| 194 |         print("Request size [B]: {:,.2f}".format(requests['size']))
 | 
|---|
| 195 |         print("")
 | 
|---|
| 196 | 
 | 
|---|
| 197 |         #==================================================
 | 
|---|
| 198 |         replies = {
 | 
|---|
| 199 |                 'min': sum([r.reply['rate']['min'] for r in results]),
 | 
|---|
| 200 |                 'avg': sum([r.reply['rate']['avg'] for r in results]),
 | 
|---|
| 201 |                 'max': sum([r.reply['rate']['max'] for r in results]),
 | 
|---|
| 202 |                 'std':  wavg([r.reply['rate']['stddev'] for r in results], [r.total['replies'] for r in results])
 | 
|---|
| 203 |         }
 | 
|---|
| 204 |         print("Reply rate [replies/s]: min {:,.2f} avg {:,.2f} max {:,.2f} avg stddev {:,.2f}".format(replies['min'], replies['avg'], replies['max'], replies['std']))
 | 
|---|
| 205 |         replies = {
 | 
|---|
| 206 |                 'rs': wavg([r.reply['time']['response'] for r in results], [r.total['replies'] for r in results]),
 | 
|---|
| 207 |                 'tr': wavg([r.reply['time']['transfer'] for r in results], [r.total['replies'] for r in results])
 | 
|---|
| 208 |         }
 | 
|---|
| 209 |         print("Reply time [ms]: response {:,.2f} transfer {:,.2f}".format(replies['rs'], replies['tr']))
 | 
|---|
| 210 |         replies = {
 | 
|---|
| 211 |                 'hd': wavg([r.reply['size']['header' ] for r in results], [r.total['replies'] for r in results]),
 | 
|---|
| 212 |                 'ct': wavg([r.reply['size']['content'] for r in results], [r.total['replies'] for r in results]),
 | 
|---|
| 213 |                 'ft': wavg([r.reply['size']['footer' ] for r in results], [r.total['replies'] for r in results]),
 | 
|---|
| 214 |                 'tt': wavg([r.reply['size']['total'  ] for r in results], [r.total['replies'] for r in results])
 | 
|---|
| 215 |         }
 | 
|---|
| 216 |         print("Reply size [B]: header {:,.2f} content {:,.2f} footer {:,.2f} (total {:,.2f})".format(replies['hd'], replies['ct'], replies['ft'], replies['tt']))
 | 
|---|
| 217 |         replies = {
 | 
|---|
| 218 |                 '1xx': sum([r.reply['status']['1xx'] for r in results]),
 | 
|---|
| 219 |                 '2xx': sum([r.reply['status']['2xx'] for r in results]),
 | 
|---|
| 220 |                 '3xx': sum([r.reply['status']['3xx'] for r in results]),
 | 
|---|
| 221 |                 '4xx': sum([r.reply['status']['4xx'] for r in results]),
 | 
|---|
| 222 |                 '5xx': sum([r.reply['status']['5xx'] for r in results])
 | 
|---|
| 223 |         }
 | 
|---|
| 224 |         print("Reply status: 1xx={:,} 2xx={:,} 3xx={:,} 4xx={:,} 5xx={:,}".format(replies['1xx'], replies['2xx'], replies['3xx'], replies['4xx'], replies['5xx']))
 | 
|---|
| 225 |         print("")
 | 
|---|
| 226 | 
 | 
|---|
| 227 |         #==================================================
 | 
|---|
| 228 |         misc = dict(functools.reduce(operator.add, map(collections.Counter, [r.misc for r in results])))
 | 
|---|
| 229 |         print("CPU time [s]: user {:.2f} system {:.2f} (user {:.2f}% system {:.2f}% total {:.2f}%)".format(misc['usr'], misc['sys'], misc['usr%'], misc['sys%'], misc['total%']))
 | 
|---|
| 230 |         print("Net I/O: {:,.2f} KB/s ({} bps)".format(misc['KB/S'], decimal.Decimal(misc['bps']).normalize().to_eng_string()))
 | 
|---|
| 231 |         print("")
 | 
|---|
| 232 | 
 | 
|---|
| 233 |         #==================================================
 | 
|---|
| 234 |         errors = dict(functools.reduce(lambda a, b: a.update(b) or a, [r.errors for r in results], collections.Counter()))
 | 
|---|
| 235 |         print("Errors: total {} client-timo {} socket-timo {} connrefused {} connreset {}".format(errors['total'], errors['client-timout'], errors['socket-timout'], errors['connection-refused'], errors['connection-reset']))
 | 
|---|
| 236 |         print("Errors: fd-unavail {} addrunavail {} ftab-full {} other {}".format(errors['fd-unavailable'], errors['address-unavailable'], errors['ftab-full'], errors['other']))
 | 
|---|
| 237 |         print("")
 | 
|---|
| 238 | 
 | 
|---|
| 239 |         #==================================================
 | 
|---|
| 240 |         sessions = {
 | 
|---|
| 241 |                 'min': sum([r.session['rate']['min'] for r in results]),
 | 
|---|
| 242 |                 'avg': wavg([r.session['rate']['avg'] for r in results], [r.session['totals'] for r in results]),
 | 
|---|
| 243 |                 'max': sum([r.session['rate']['max'] for r in results]),
 | 
|---|
| 244 |                 'stddev':  wavg([r.session['rate']['stddev'] for r in results], [r.session['totals'] for r in results]),
 | 
|---|
| 245 |                 'successes': sum([r.session['successes'] for r in results]),
 | 
|---|
| 246 |                 'totals': sum([r.session['totals'] for r in results]),
 | 
|---|
| 247 |                 'conns/ses': wavg([r.session['conns/ses'] for r in results], [r.session['totals'] for r in results]),
 | 
|---|
| 248 |                 'lifetime': wavg([r.session['lifetime'] for r in results], [r.session['successes'] for r in results]),
 | 
|---|
| 249 |                 'failtime': wavg([r.session['failtime'] for r in results], [r.session['totals'] - r.session['successes'] for r in results]),
 | 
|---|
| 250 |         }
 | 
|---|
| 251 |         print("Session rate [sess/s]: min {:.2f} avg {:.2f} max {:.2f} avg stddev {:.2f} ({:,}/{:,})".format(sessions['min'], sessions['avg'], sessions['max'], sessions['stddev'], sessions['successes'], sessions['totals']))
 | 
|---|
| 252 |         print("Session: avg {:.2f} connections/session".format(sessions['conns/ses']))
 | 
|---|
| 253 |         print("Session lifetime [s]: {:.2f}".format(sessions['lifetime']))
 | 
|---|
| 254 |         print("Session failtime [s]: {:.2f}".format(sessions['failtime']))
 | 
|---|
| 255 | 
 | 
|---|
| 256 |         hist = dict(functools.reduce(operator.add, map(collections.Counter, [r.session['hist'] for r in results])))
 | 
|---|
| 257 |         hist = ["{}: {}".format(key, value) for key, value in sorted(hist.items(), key=lambda x: x[0])]
 | 
|---|
| 258 |         print("Session length histogram: [{}]".format(", ".join(hist)))
 | 
|---|