Line length: Fix recent merges
[csit.git] / resources / tools / ab / ABFork.py
1 #!/usr/bin/env python3
2 # Copyright (c) 2021 Intel and/or its affiliates.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at:
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """ab fork library."""
16
17 from multiprocessing import Pool
18 import subprocess
19 import argparse
20 import re
21
22 REGEX_RPS = r"Requests per second:\s*" \
23             r"(\d*\.*\S*)"
24 REGEX_LATENCY = r"Time per request:\s*" \
25                 r"(\d*\.*\S*)"
26 REGEX_PROCESS = r"Time per request:\s*" \
27                 r"(\d*\.*\S*)"
28 REGEX_TR = r"Transfer rate:\s*" \
29            r"(\d*\.*\S*)"
30 REGEX_TT = r"Total transferred:\s*" \
31            r"(\d*)"
32 REGEX_OK_NUM = r"Complete requests:\s*" \
33                r"(\d*)"
34 REGEX_FAILED_NUM = r"Failed requests:\s*" \
35                    r"(\d*)"
36 REGEX_NUM = r"(\d*\.*\d*)(\D*)"
37
38
39 def main():
40     """ main function. get option and run ab test.
41
42     :returns: Nothing.
43     """
44
45     # Get option.
46     parser = argparse.ArgumentParser(description=u"Get option and run ab test")
47
48     # Number of requests to perform.
49     parser.add_argument(u"-r", u"--requests", type=int,
50                         required=True, help=u"Number of requests to perform.")
51
52     # Server port number to use.
53     parser.add_argument(u"-p", u"--port", type=int, required=True,
54                         help=u"Server port number to use.")
55
56     # Number of clients being processed at the same time.
57     parser.add_argument(u"-c", u"--clients", type=int, required=True,
58                         help=u"Number of clients being processed at "
59                              u"the same time.")
60
61     # Filename to be requested from the servers.
62     parser.add_argument(u"-f", u"--files", type=str, required=True,
63                         help="Filename to be requested from the servers.")
64
65     # Server ip address.
66     parser.add_argument(u"-i", u"--ip", type=str, required=True,
67                         help=u"Server bind IP address.")
68
69     # Tg ip address.
70     parser.add_argument(u"-g", u"--tip", type=str, required=True,
71                         help=u"TG bind IP address.")
72
73     # Specify SSL/TLS cipher suite.
74     parser.add_argument(u"-z", u"--cipher", type=str, default=u"0",
75                         help=u"Specify SSL/TLS cipher.")
76
77     # Specify SSL/TLS protocol.
78     parser.add_argument(u"-t", u"--protocol", type=str, default=u"0",
79                         help=u"Specify SSL/TLS protocol.")
80
81     # Mode: RPS or CPS.
82     parser.add_argument(u"-m", u"--mode", type=str, required=True,
83                         help=u"Send requests mode:RPS/CPS.")
84
85     args = parser.parse_args()
86
87     req_num = args.requests
88     port = args.port
89     cli_num = args.clients
90     files = args.files
91     ip_address = args.ip
92     tg_address = args.tip
93     cipher = args.cipher
94     protocol = args.protocol
95     mode = args.mode
96
97     if req_num == 0:
98         print(u"Failed number of req_num!")
99         return 1
100
101     # The number of processing units available to the current process.
102     _, cpu_num = subprocess.getstatusoutput(u"nproc --all")
103     cpu_num = int(cpu_num)
104     if cpu_num > 70:
105         cpu_num = 70
106
107     # Requests and Clients are evenly distributed on each CPU.
108     per_req = round(req_num / cpu_num)
109     per_cli = round(cli_num / cpu_num)
110
111     # Revise rounding request, This will be in the first ab request
112     all_total = per_req * cpu_num
113     per_req_1st = per_req + (req_num - all_total)
114
115     results = []
116     # Start process pool.
117     pool = Pool(processes=cpu_num)
118
119     for i in range(1, cpu_num + 1):
120         results.append(
121             pool.apply_async(one, (
122                 i, per_req_1st if i == 1 else per_req, per_cli, cipher,
123                 protocol, ip_address, tg_address, files, port, mode)))
124
125     pool.close()
126     pool.join()
127
128     info_list = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
129
130     # Statistical test results.
131     for res in results:
132         stats = res.get()
133         if stats:
134             info_list = [a + b for a, b in zip(info_list, stats)]
135
136     # Output results.
137     print(f"Transfer Rate: {round(info_list[6], 2)} [Kbytes/sec]")
138     print(f"Latency: {round(info_list[4] / 8, 2)} ms")
139     print(f"Connection {mode} rate:{round(info_list[3], 2)} per sec")
140     print(f"Total data transferred: {round(info_list[2])} bytes")
141     print(f"Completed requests: {round(info_list[0])} ")
142     print(f"Failed requests: {round(info_list[1])} ")
143
144
145 def one(cpu, requests, clients, cipher, protocol, ip_addr, tg_addr, files, port,
146         mode):
147     """Run one test.
148
149     :param cpu: Core number id.
150     :param requests: Request number.
151     :param clients: Clients number.
152     :param cipher: Specify SSL/TLS cipher suite.
153     :param protocol: Specify SSL/TLS protocol.
154     :param ip_addr: Server ip address.
155     :param tg_addr: Tg ip address.
156     :param files: Filename to be requested from the servers.
157     :param port: Server port.
158     :type cpu: int
159     :type requests: int
160     :type clients: int
161     :type cipher: str
162     :type protocol: str
163     :type ip_addr: str
164     :type tg_addr: str
165     :type files: str
166     :type port: int
167     :type mode: str
168     :returns: Test results.
169     :rtype: list
170     """
171
172     cmd = f"sudo -E -S taskset --cpu-list {cpu} ab -n {requests} -c {clients}"
173     cmd = f"{cmd} -B {tg_addr} -r "
174     if mode == u"rps":
175         cmd = f"{cmd} -k"
176
177     if port == 80:
178         cmd = f"{cmd} http://{ip_addr}:{port}/{files}"
179     else:
180         cmd = f"{cmd} -Z {cipher} -f {protocol}"
181         cmd = f"{cmd} https://{ip_addr}:{port}/{files}"
182
183     _, output = subprocess.getstatusoutput(cmd)
184     ret = _parse_output(output)
185
186     return ret
187
188
189 def _parse_output(msg):
190     """Parse the stdout with the results.
191
192     :param msg: stdout of ab.
193     :type msg: str
194     :returns: Parsed results.
195     :rtype: list
196     """
197
198     msg_lst = msg.splitlines(False)
199
200     stats = []
201     for line in msg_lst:
202         if u"Requests per second" in line:
203             stats.append(
204                 _float_number(re.search(REGEX_RPS, line).group(1))
205             )
206         elif u"Time per request" in line:
207             stats.append(
208                 _float_number(re.search(REGEX_LATENCY, line).group(1))
209             )
210         elif u"Transfer rate" in line:
211             stats.append(
212                 _float_number(re.search(REGEX_TR, line).group(1))
213             )
214         elif u"Total transferred" in line:
215             stats.append(
216                 _float_number(re.search(REGEX_TT, line).group(1))
217             )
218         elif u"Complete requests" in line:
219             stats.append(
220                 _float_number(re.search(REGEX_OK_NUM, line).group(1))
221             )
222         elif u"Failed requests" in line:
223             stats.append(
224                 _float_number(re.search(REGEX_FAILED_NUM, line).group(1))
225             )
226
227     return stats
228
229
230 def _float_number(num):
231     """float value of the number.
232
233     :param num: Number to evaluate.
234     :type num: str
235     :returns: float number.
236     :rtype: float
237     """
238
239     val = re.search(REGEX_NUM, num)
240     try:
241         val_num = float(val.group(1))
242     except ValueError:
243         raise RuntimeError(u"The output of ab does not include the results.")
244     return val_num
245
246
247 if __name__ == "__main__":
248     main()