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