VAT-to-PAPI: Fix HTTP/TCP tests
[csit.git] / resources / tools / wrk / wrk.py
1 # Copyright (c) 2019 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """wrk implementation into CSIT framework.
15 """
16
17 import re
18
19 from copy import deepcopy
20 from time import sleep
21
22 from robot.api import logger
23
24 from resources.libraries.python.ssh import SSH
25 from resources.libraries.python.topology import NodeType
26 from resources.libraries.python.CpuUtils import CpuUtils
27 from resources.libraries.python.Constants import Constants
28
29 from resources.tools.wrk.wrk_traffic_profile_parser import WrkTrafficProfile
30 from resources.tools.wrk.wrk_errors import WrkError
31
32
33 REGEX_LATENCY_STATS = \
34     r"Latency\s*" \
35     r"(\d*\.*\d*\S*)\s*" \
36     r"(\d*\.*\d*\S*)\s*" \
37     r"(\d*\.*\d*\S*)\s*" \
38     r"(\d*\.*\d*\%)"
39 REGEX_RPS_STATS = \
40     r"Req/Sec\s*" \
41     r"(\d*\.*\d*\S*)\s*" \
42     r"(\d*\.*\d*\S*)\s*" \
43     r"(\d*\.*\d*\S*)\s*" \
44     r"(\d*\.*\d*\%)"
45 REGEX_RPS = r"Requests/sec:\s*" \
46             r"(\d*\.*\S*)"
47 REGEX_BW = r"Transfer/sec:\s*" \
48            r"(\d*\.*\S*)"
49 REGEX_LATENCY_DIST = \
50     r"Latency Distribution\n" \
51     r"\s*50\%\s*(\d*\.*\d*\D*)\n" \
52     r"\s*75\%\s*(\d*\.*\d*\D*)\n" \
53     r"\s*90\%\s*(\d*\.*\d*\D*)\n" \
54     r"\s*99\%\s*(\d*\.*\d*\D*)\n"
55
56 # Split number and multiplicand, e.g. 14.25k --> 14.25 and k
57 REGEX_NUM = r"(\d*\.*\d*)(\D*)"
58
59
60 def check_wrk(tg_node):
61     """Check if wrk is installed on the TG node.
62
63     :param tg_node: Traffic generator node.
64     :type tg_node: dict
65     :raises: RuntimeError if the given node is not a TG node or if the
66         command is not availble.
67     """
68
69     if tg_node['type'] != NodeType.TG:
70         raise RuntimeError('Node type is not a TG.')
71
72     ssh = SSH()
73     ssh.connect(tg_node)
74
75     ret, _, _ = ssh.exec_command(
76         "sudo -E "
77         "sh -c '{0}/resources/tools/wrk/wrk_utils.sh installed'".
78         format(Constants.REMOTE_FW_DIR))
79     if int(ret) != 0:
80         raise RuntimeError('WRK is not installed on TG node.')
81
82
83 def run_wrk(tg_node, profile_name, tg_numa, test_type, warm_up=False):
84     """Send the traffic as defined in the profile.
85
86     :param tg_node: Traffic generator node.
87     :param profile_name: The name of wrk traffic profile.
88     :param tg_numa: Numa node on which wrk will run.
89     :param test_type: The type of the tests: cps, rps, bw
90     :param warm_up: If True, warm-up traffic is generated before test traffic.
91     :type profile_name: str
92     :type tg_node: dict
93     :type tg_numa: int
94     :type test_type: str
95     :type warm_up: bool
96     :returns: Message with measured data.
97     :rtype: str
98     :raises: RuntimeError if node type is not a TG.
99     """
100
101     if tg_node['type'] != NodeType.TG:
102         raise RuntimeError('Node type is not a TG.')
103
104     # Parse and validate the profile
105     profile_path = ("resources/traffic_profiles/wrk/{0}.yaml".
106                     format(profile_name))
107     profile = WrkTrafficProfile(profile_path).traffic_profile
108
109     cores = CpuUtils.cpu_list_per_node(tg_node, tg_numa)
110     first_cpu = cores[profile["first-cpu"]]
111
112     if len(profile["urls"]) == 1 and profile["cpus"] == 1:
113         params = [
114             "traffic_1_url_1_core",
115             str(first_cpu),
116             str(profile["nr-of-threads"]),
117             str(profile["nr-of-connections"]),
118             "{0}s".format(profile["duration"]),
119             "'{0}'".format(profile["header"]),
120             str(profile["timeout"]),
121             str(profile["script"]),
122             str(profile["latency"]),
123             "'{0}'".format(" ".join(profile["urls"]))
124         ]
125         if warm_up:
126             warm_up_params = deepcopy(params)
127             warm_up_params[4] = "10s"
128     elif len(profile["urls"]) == profile["cpus"]:
129         params = [
130             "traffic_n_urls_n_cores",
131             str(first_cpu),
132             str(profile["nr-of-threads"]),
133             str(profile["nr-of-connections"]),
134             "{0}s".format(profile["duration"]),
135             "'{0}'".format(profile["header"]),
136             str(profile["timeout"]),
137             str(profile["script"]),
138             str(profile["latency"]),
139             "'{0}'".format(" ".join(profile["urls"]))
140         ]
141         if warm_up:
142             warm_up_params = deepcopy(params)
143             warm_up_params[4] = "10s"
144     else:
145         params = [
146             "traffic_n_urls_m_cores",
147             str(first_cpu),
148             str(profile["cpus"] / len(profile["urls"])),
149             str(profile["nr-of-threads"]),
150             str(profile["nr-of-connections"]),
151             "{0}s".format(profile["duration"]),
152             "'{0}'".format(profile["header"]),
153             str(profile["timeout"]),
154             str(profile["script"]),
155             str(profile["latency"]),
156             "'{0}'".format(" ".join(profile["urls"]))
157         ]
158         if warm_up:
159             warm_up_params = deepcopy(params)
160             warm_up_params[5] = "10s"
161
162     args = " ".join(params)
163
164     ssh = SSH()
165     ssh.connect(tg_node)
166
167     if warm_up:
168         warm_up_args = " ".join(warm_up_params)
169         ret, _, _ = ssh.exec_command(
170             "{0}/resources/tools/wrk/wrk_utils.sh {1}".
171             format(Constants.REMOTE_FW_DIR, warm_up_args), timeout=1800)
172         if int(ret) != 0:
173             raise RuntimeError('wrk runtime error.')
174         sleep(60)
175
176     ret, stdout, _ = ssh.exec_command(
177         "{0}/resources/tools/wrk/wrk_utils.sh {1}".
178         format(Constants.REMOTE_FW_DIR, args), timeout=1800)
179     if int(ret) != 0:
180         raise RuntimeError('wrk runtime error.')
181
182     stats = _parse_wrk_output(stdout)
183
184     log_msg = "\nMeasured values:\n"
185     if test_type == "cps":
186         log_msg += "Connections/sec: Avg / Stdev / Max  / +/- Stdev\n"
187         for item in stats["rps-stats-lst"]:
188             log_msg += "{0} / {1} / {2} / {3}\n".format(*item)
189         log_msg += "Total cps: {0}cps\n".format(stats["rps-sum"])
190     elif test_type == "rps":
191         log_msg += "Requests/sec: Avg / Stdev / Max  / +/- Stdev\n"
192         for item in stats["rps-stats-lst"]:
193             log_msg += "{0} / {1} / {2} / {3}\n".format(*item)
194         log_msg += "Total rps: {0}rps\n".format(stats["rps-sum"])
195     elif test_type == "bw":
196         log_msg += "Transfer/sec: {0}Bps".format(stats["bw-sum"])
197
198     logger.info(log_msg)
199
200     return log_msg
201
202
203 def _parse_wrk_output(msg):
204     """Parse the wrk stdout with the results.
205
206     :param msg: stdout of wrk.
207     :type msg: str
208     :returns: Parsed results.
209     :rtype: dict
210     :raises: WrkError if the message does not include the results.
211     """
212
213     if "Thread Stats" not in msg:
214         raise WrkError("The output of wrk does not include the results.")
215
216     msg_lst = msg.splitlines(False)
217
218     stats = {
219         "latency-dist-lst": list(),
220         "latency-stats-lst": list(),
221         "rps-stats-lst": list(),
222         "rps-lst": list(),
223         "bw-lst": list(),
224         "rps-sum": 0,
225         "bw-sum": None
226     }
227
228     for line in msg_lst:
229         if "Latency Distribution" in line:
230             # Latency distribution - 50%, 75%, 90%, 99%
231             pass
232         elif "Latency" in line:
233             # Latency statistics - Avg, Stdev, Max, +/- Stdev
234             pass
235         elif "Req/Sec" in line:
236             # rps statistics - Avg, Stdev, Max, +/- Stdev
237             stats["rps-stats-lst"].append((
238                 _evaluate_number(re.search(REGEX_RPS_STATS, line).group(1)),
239                 _evaluate_number(re.search(REGEX_RPS_STATS, line).group(2)),
240                 _evaluate_number(re.search(REGEX_RPS_STATS, line).group(3)),
241                 _evaluate_number(re.search(REGEX_RPS_STATS, line).group(4))))
242         elif "Requests/sec:" in line:
243             # rps (cps)
244             stats["rps-lst"].append(
245                 _evaluate_number(re.search(REGEX_RPS, line).group(1)))
246         elif "Transfer/sec:" in line:
247             # BW
248             stats["bw-lst"].append(
249                 _evaluate_number(re.search(REGEX_BW, line).group(1)))
250
251     for item in stats["rps-stats-lst"]:
252         stats["rps-sum"] += item[0]
253     stats["bw-sum"] = sum(stats["bw-lst"])
254
255     return stats
256
257
258 def _evaluate_number(num):
259     """Evaluate the numeric value of the number with multiplicands, e.g.:
260     12.25k --> 12250
261
262     :param num: Number to evaluate.
263     :type num: str
264     :returns: Evaluated number.
265     :rtype: float
266     :raises: WrkError if it is not possible to evaluate the given number.
267     """
268
269     val = re.search(REGEX_NUM, num)
270     try:
271         val_num = float(val.group(1))
272     except ValueError:
273         raise WrkError("The output of wrk does not include the results "
274                        "or the format of results has changed.")
275     val_mul = val.group(2).lower()
276     if val_mul:
277         if "k" in val_mul:
278             val_num *= 1000
279         elif "m" in val_mul:
280             val_num *= 1000000
281         elif "g" in val_mul:
282             val_num *= 1000000000
283         elif "b" in val_mul:
284             pass
285         elif "%" in val_mul:
286             pass
287         elif "" in val_mul:
288             pass
289         else:
290             raise WrkError("The multiplicand {0} is not defined.".
291                            format(val_mul))
292     return val_num