[cd70477] | 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))) |
---|