| 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))) | 
|---|