Line length: Fix recent merges
[csit.git] / resources / libraries / python / CpuUtils.py
1 # Copyright (c) 2021 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 """CPU utilities library."""
15
16 from robot.libraries.BuiltIn import BuiltIn
17
18 from resources.libraries.python.Constants import Constants
19 from resources.libraries.python.ssh import exec_cmd_no_error
20 from resources.libraries.python.topology import Topology
21
22 __all__ = [u"CpuUtils"]
23
24
25 class CpuUtils:
26     """CPU utilities"""
27
28     # Number of threads per core.
29     NR_OF_THREADS = 2
30
31     @staticmethod
32     def __str2int(string):
33         """Conversion from string to integer, 0 in case of empty string.
34
35         :param string: Input string.
36         :type string: str
37         :returns: Integer converted from string, 0 in case of ValueError.
38         :rtype: int
39         """
40         try:
41             return int(string)
42         except ValueError:
43             return 0
44
45     @staticmethod
46     def is_smt_enabled(cpu_info):
47         """Uses CPU mapping to find out if SMT is enabled or not. If SMT is
48         enabled, the L1d,L1i,L2,L3 setting is the same for two processors. These
49         two processors are two threads of one core.
50
51         :param cpu_info: CPU info, the output of "lscpu -p".
52         :type cpu_info: list
53         :returns: True if SMT is enabled, False if SMT is disabled.
54         :rtype: bool
55         """
56         cpu_mems = [item[-4:] for item in cpu_info]
57         cpu_mems_len = len(cpu_mems) // CpuUtils.NR_OF_THREADS
58         count = 0
59         for cpu_mem in cpu_mems[:cpu_mems_len]:
60             if cpu_mem in cpu_mems[cpu_mems_len:]:
61                 count += 1
62         return bool(count == cpu_mems_len)
63
64     @staticmethod
65     def get_cpu_info_from_all_nodes(nodes):
66         """Assuming all nodes are Linux nodes, retrieve the following
67            cpu information from all nodes:
68                - cpu architecture
69                - cpu layout
70
71         :param nodes: DICT__nodes from Topology.DICT__nodes.
72         :type nodes: dict
73         :raises RuntimeError: If an ssh command retrieving cpu information
74             fails.
75         """
76         for node in nodes.values():
77             stdout, _ = exec_cmd_no_error(node, u"uname -m")
78             node[u"arch"] = stdout.strip()
79             stdout, _ = exec_cmd_no_error(node, u"lscpu -p")
80             node[u"cpuinfo"] = list()
81             for line in stdout.split(u"\n"):
82                 if line and line[0] != u"#":
83                     node[u"cpuinfo"].append(
84                         [CpuUtils.__str2int(x) for x in line.split(u",")]
85                     )
86
87     @staticmethod
88     def worker_count_from_cores_and_smt(phy_cores, smt_used):
89         """Simple conversion utility, needs smt from caller.
90
91         The implementation assumes we pack 1 or 2 workers per core,
92         depending on hyperthreading.
93
94         Some keywords use None to indicate no core/worker limit,
95         so this converts None to None.
96
97         :param phy_cores: How many physical cores to use for workers.
98         :param smt_used: Whether symmetric multithreading is used.
99         :type phy_cores: Optional[int]
100         :type smt_used: bool
101         :returns: How many VPP workers fit into the given number of cores.
102         :rtype: Optional[int]
103         """
104         if phy_cores is None:
105             return None
106         workers_per_core = CpuUtils.NR_OF_THREADS if smt_used else 1
107         workers = phy_cores * workers_per_core
108         return workers
109
110     @staticmethod
111     def cpu_node_count(node):
112         """Return count of numa nodes.
113
114         :param node: Targeted node.
115         :type node: dict
116         :returns: Count of numa nodes.
117         :rtype: int
118         :raises RuntimeError: If node cpuinfo is not available.
119         """
120         cpu_info = node.get(u"cpuinfo")
121         if cpu_info is not None:
122             return node[u"cpuinfo"][-1][3] + 1
123
124         raise RuntimeError(u"Node cpuinfo not available.")
125
126     @staticmethod
127     def cpu_list_per_node(node, cpu_node, smt_used=False):
128         """Return node related list of CPU numbers.
129
130         :param node: Node dictionary with cpuinfo.
131         :param cpu_node: Numa node number.
132         :param smt_used: True - we want to use SMT, otherwise false.
133         :type node: dict
134         :type cpu_node: int
135         :type smt_used: bool
136         :returns: List of cpu numbers related to numa from argument.
137         :rtype: list of int
138         :raises RuntimeError: If node cpuinfo is not available
139             or if SMT is not enabled.
140         """
141         cpu_node = int(cpu_node)
142         cpu_info = node.get(u"cpuinfo")
143         if cpu_info is None:
144             raise RuntimeError(u"Node cpuinfo not available.")
145
146         smt_enabled = CpuUtils.is_smt_enabled(cpu_info)
147         if not smt_enabled and smt_used:
148             raise RuntimeError(u"SMT is not enabled.")
149
150         cpu_list = []
151         for cpu in cpu_info:
152             if cpu[3] == cpu_node:
153                 cpu_list.append(cpu[0])
154
155         if not smt_enabled or smt_enabled and smt_used:
156             pass
157
158         if smt_enabled and not smt_used:
159             cpu_list_len = len(cpu_list)
160             cpu_list = cpu_list[:cpu_list_len // CpuUtils.NR_OF_THREADS]
161
162         return cpu_list
163
164     @staticmethod
165     def cpu_slice_of_list_per_node(
166             node, cpu_node, skip_cnt=0, cpu_cnt=0, smt_used=False):
167         """Return node related subset of list of CPU numbers.
168
169         :param node: Node dictionary with cpuinfo.
170         :param cpu_node: Numa node number.
171         :param skip_cnt: Skip first "skip_cnt" CPUs.
172         :param cpu_cnt: Count of cpus to return, if 0 then return all.
173         :param smt_used: True - we want to use SMT, otherwise false.
174         :type node: dict
175         :type cpu_node: int
176         :type skip_cnt: int
177         :type cpu_cnt: int
178         :type smt_used: bool
179         :returns: Cpu numbers related to numa from argument.
180         :rtype: list
181         :raises RuntimeError: If we require more cpus than available.
182         """
183         cpu_list = CpuUtils.cpu_list_per_node(node, cpu_node, smt_used)
184
185         cpu_list_len = len(cpu_list)
186         if cpu_cnt + skip_cnt > cpu_list_len:
187             raise RuntimeError(u"cpu_cnt + skip_cnt > length(cpu list).")
188
189         if cpu_cnt == 0:
190             cpu_cnt = cpu_list_len - skip_cnt
191
192         if smt_used:
193             cpu_list_0 = cpu_list[:cpu_list_len // CpuUtils.NR_OF_THREADS]
194             cpu_list_1 = cpu_list[cpu_list_len // CpuUtils.NR_OF_THREADS:]
195             cpu_list = cpu_list_0[skip_cnt:skip_cnt + cpu_cnt]
196             cpu_list_ex = cpu_list_1[skip_cnt:skip_cnt + cpu_cnt]
197             cpu_list.extend(cpu_list_ex)
198         else:
199             cpu_list = cpu_list[skip_cnt:skip_cnt + cpu_cnt]
200
201         return cpu_list
202
203     @staticmethod
204     def cpu_list_per_node_str(
205             node, cpu_node, skip_cnt=0, cpu_cnt=0, sep=u",", smt_used=False):
206         """Return string of node related list of CPU numbers.
207
208         :param node: Node dictionary with cpuinfo.
209         :param cpu_node: Numa node number.
210         :param skip_cnt: Skip first "skip_cnt" CPUs.
211         :param cpu_cnt: Count of cpus to return, if 0 then return all.
212         :param sep: Separator, default: 1,2,3,4,....
213         :param smt_used: True - we want to use SMT, otherwise false.
214         :type node: dict
215         :type cpu_node: int
216         :type skip_cnt: int
217         :type cpu_cnt: int
218         :type sep: str
219         :type smt_used: bool
220         :returns: Cpu numbers related to numa from argument.
221         :rtype: str
222         """
223         cpu_list = CpuUtils.cpu_slice_of_list_per_node(
224             node, cpu_node, skip_cnt=skip_cnt, cpu_cnt=cpu_cnt,
225             smt_used=smt_used
226         )
227         return sep.join(str(cpu) for cpu in cpu_list)
228
229     @staticmethod
230     def cpu_range_per_node_str(
231             node, cpu_node, skip_cnt=0, cpu_cnt=0, sep=u"-", smt_used=False):
232         """Return string of node related range of CPU numbers, e.g. 0-4.
233
234         :param node: Node dictionary with cpuinfo.
235         :param cpu_node: Numa node number.
236         :param skip_cnt: Skip first "skip_cnt" CPUs.
237         :param cpu_cnt: Count of cpus to return, if 0 then return all.
238         :param sep: Separator, default: "-".
239         :param smt_used: True - we want to use SMT, otherwise false.
240         :type node: dict
241         :type cpu_node: int
242         :type skip_cnt: int
243         :type cpu_cnt: int
244         :type sep: str
245         :type smt_used: bool
246         :returns: String of node related range of CPU numbers.
247         :rtype: str
248         """
249         cpu_list = CpuUtils.cpu_slice_of_list_per_node(
250             node, cpu_node, skip_cnt=skip_cnt, cpu_cnt=cpu_cnt,
251             smt_used=smt_used
252         )
253         if smt_used:
254             cpu_list_len = len(cpu_list)
255             cpu_list_0 = cpu_list[:cpu_list_len // CpuUtils.NR_OF_THREADS]
256             cpu_list_1 = cpu_list[cpu_list_len // CpuUtils.NR_OF_THREADS:]
257             cpu_range = f"{cpu_list_0[0]}{sep}{cpu_list_0[-1]}," \
258                         f"{cpu_list_1[0]}{sep}{cpu_list_1[-1]}"
259         else:
260             cpu_range = f"{cpu_list[0]}{sep}{cpu_list[-1]}"
261
262         return cpu_range
263
264     @staticmethod
265     def cpu_slice_of_list_for_nf(
266             node, cpu_node, nf_chains=1, nf_nodes=1, nf_chain=1, nf_node=1,
267             nf_dtc=1, nf_mtcr=2, nf_dtcr=1, skip_cnt=0):
268         """Return list of DUT node related list of CPU numbers. The main
269         computing unit is physical core count.
270
271         :param node: DUT node.
272         :param cpu_node: Numa node number.
273         :param nf_chains: Number of NF chains.
274         :param nf_nodes: Number of NF nodes in chain.
275         :param nf_chain: Chain number indexed from 1.
276         :param nf_node: Node number indexed from 1.
277         :param nf_dtc: Amount of physical cores for NF data plane.
278         :param nf_mtcr: NF main thread per core ratio.
279         :param nf_dtcr: NF data plane thread per core ratio.
280         :param skip_cnt: Skip first "skip_cnt" CPUs.
281         :type node: dict
282         :param cpu_node: int.
283         :type nf_chains: int
284         :type nf_nodes: int
285         :type nf_chain: int
286         :type nf_node: int
287         :type nf_dtc: int or float
288         :type nf_mtcr: int
289         :type nf_dtcr: int
290         :type skip_cnt: int
291         :returns: List of CPUs allocated to NF.
292         :rtype: list
293         :raises RuntimeError: If we require more cpus than available or if
294         placement is not possible due to wrong parameters.
295         """
296         if not 1 <= nf_chain <= nf_chains:
297             raise RuntimeError(u"ChainID is out of range!")
298         if not 1 <= nf_node <= nf_nodes:
299             raise RuntimeError(u"NodeID is out of range!")
300
301         smt_used = CpuUtils.is_smt_enabled(node[u"cpuinfo"])
302         cpu_list = CpuUtils.cpu_list_per_node(node, cpu_node, smt_used)
303         # CPU thread sibling offset.
304         sib = len(cpu_list) // CpuUtils.NR_OF_THREADS
305
306         dtc_is_integer = isinstance(nf_dtc, int)
307         if not smt_used and not dtc_is_integer:
308             raise RuntimeError(u"Cannot allocate if SMT is not enabled!")
309         if not dtc_is_integer:
310             nf_dtc = 1
311
312         mt_req = ((nf_chains * nf_nodes) + nf_mtcr - 1) // nf_mtcr
313         dt_req = ((nf_chains * nf_nodes) + nf_dtcr - 1) // nf_dtcr
314
315         if (skip_cnt + mt_req + dt_req) > (sib if smt_used else len(cpu_list)):
316             raise RuntimeError(u"Not enough CPU cores available for placement!")
317
318         offset = (nf_node - 1) + (nf_chain - 1) * nf_nodes
319         mt_skip = skip_cnt + (offset % mt_req)
320         dt_skip = skip_cnt + mt_req + (offset % dt_req) * nf_dtc
321
322         result = cpu_list[dt_skip:dt_skip + nf_dtc]
323         if smt_used:
324             if (offset // mt_req) & 1:  # check oddness
325                 mt_skip += sib
326
327             dt_skip += sib
328             if dtc_is_integer:
329                 result.extend(cpu_list[dt_skip:dt_skip + nf_dtc])
330             elif (offset // dt_req) & 1:  # check oddness
331                 result = cpu_list[dt_skip:dt_skip + nf_dtc]
332
333         result[0:0] = cpu_list[mt_skip:mt_skip + 1]
334         return result
335
336     @staticmethod
337     def get_affinity_af_xdp(
338             node, pf_key, cpu_skip_cnt=0, cpu_cnt=1):
339         """Get affinity for AF_XDP interface. Result will be used to pin IRQs.
340
341         :param node: Topology node.
342         :param pf_key: Topology interface.
343         :param cpu_skip_cnt: Amount of CPU cores to skip.
344         :param cpu_cnt: CPU threads count.
345         :type node: dict
346         :type pf_key: str
347         :type cpu_skip_cnt: int
348         :type cpu_cnt: int
349         :returns: List of CPUs allocated to AF_XDP interface.
350         :rtype: list
351         """
352         if pf_key:
353             cpu_node = Topology.get_interface_numa_node(node, pf_key)
354         else:
355             cpu_node = 0
356
357         smt_used = CpuUtils.is_smt_enabled(node[u"cpuinfo"])
358         if smt_used:
359             cpu_cnt = cpu_cnt // CpuUtils.NR_OF_THREADS
360
361         return CpuUtils.cpu_slice_of_list_per_node(
362             node, cpu_node, skip_cnt=cpu_skip_cnt, cpu_cnt=cpu_cnt,
363             smt_used=smt_used
364         )
365
366     @staticmethod
367     def get_affinity_nf(
368             nodes, node, nf_chains=1, nf_nodes=1, nf_chain=1, nf_node=1,
369             vs_dtc=1, nf_dtc=1, nf_mtcr=2, nf_dtcr=1):
370
371         """Get affinity of NF (network function). Result will be used to compute
372         the amount of CPUs and also affinity.
373
374         :param nodes: Physical topology nodes.
375         :param node: SUT node.
376         :param nf_chains: Number of NF chains.
377         :param nf_nodes: Number of NF nodes in chain.
378         :param nf_chain: Chain number indexed from 1.
379         :param nf_node: Node number indexed from 1.
380         :param vs_dtc: Amount of physical cores for vswitch data plane.
381         :param nf_dtc: Amount of physical cores for NF data plane.
382         :param nf_mtcr: NF main thread per core ratio.
383         :param nf_dtcr: NF data plane thread per core ratio.
384         :type nodes: dict
385         :type node: dict
386         :type nf_chains: int
387         :type nf_nodes: int
388         :type nf_chain: int
389         :type nf_node: int
390         :type vs_dtc: int
391         :type nf_dtc: int or float
392         :type nf_mtcr: int
393         :type nf_dtcr: int
394         :returns: List of CPUs allocated to NF.
395         :rtype: list
396         """
397         skip_cnt = Constants.CPU_CNT_SYSTEM + Constants.CPU_CNT_MAIN + vs_dtc
398
399         interface_list = list()
400         interface_list.append(BuiltIn().get_variable_value(f"${{{node}_if1}}"))
401         interface_list.append(BuiltIn().get_variable_value(f"${{{node}_if2}}"))
402
403         cpu_node = Topology.get_interfaces_numa_node(
404             nodes[node], *interface_list)
405
406         return CpuUtils.cpu_slice_of_list_for_nf(
407             node=nodes[node], cpu_node=cpu_node, nf_chains=nf_chains,
408             nf_nodes=nf_nodes, nf_chain=nf_chain, nf_node=nf_node,
409             nf_mtcr=nf_mtcr, nf_dtcr=nf_dtcr, nf_dtc=nf_dtc, skip_cnt=skip_cnt
410         )
411
412     @staticmethod
413     def get_affinity_trex(
414             node, if1_pci, if2_pci, tg_mtc=1, tg_dtc=1, tg_ltc=1):
415         """Get affinity for T-Rex. Result will be used to pin T-Rex threads.
416
417         :param node: TG node.
418         :param if1_pci: TG first interface.
419         :param if2_pci: TG second interface.
420         :param tg_mtc: TG main thread count.
421         :param tg_dtc: TG dataplane thread count.
422         :param tg_ltc: TG latency thread count.
423         :type node: dict
424         :type if1_pci: str
425         :type if2_pci: str
426         :type tg_mtc: int
427         :type tg_dtc: int
428         :type tg_ltc: int
429         :returns: List of CPUs allocated to T-Rex including numa node.
430         :rtype: int, int, int, list
431         """
432         interface_list = [if1_pci, if2_pci]
433         cpu_node = Topology.get_interfaces_numa_node(node, *interface_list)
434
435         master_thread_id = CpuUtils.cpu_slice_of_list_per_node(
436             node, cpu_node, skip_cnt=0, cpu_cnt=tg_mtc,
437             smt_used=False)
438
439         threads = CpuUtils.cpu_slice_of_list_per_node(
440             node, cpu_node, skip_cnt=tg_mtc, cpu_cnt=tg_dtc,
441             smt_used=False)
442
443         latency_thread_id = CpuUtils.cpu_slice_of_list_per_node(
444             node, cpu_node, skip_cnt=tg_mtc + tg_dtc, cpu_cnt=tg_ltc,
445             smt_used=False)
446
447         return master_thread_id[0], latency_thread_id[0], cpu_node, threads
448
449     @staticmethod
450     def get_affinity_iperf(
451             node, pf_key, cpu_skip_cnt=0, cpu_cnt=1):
452         """Get affinity for iPerf3. Result will be used to pin iPerf3 threads.
453
454         :param node: Topology node.
455         :param pf_key: Topology interface.
456         :param cpu_skip_cnt: Amount of CPU cores to skip.
457         :param cpu_cnt: CPU threads count.
458         :type node: dict
459         :type pf_key: str
460         :type cpu_skip_cnt: int
461         :type cpu_cnt: int
462         :returns: List of CPUs allocated to iPerf3.
463         :rtype: str
464         """
465         if pf_key:
466             cpu_node = Topology.get_interface_numa_node(node, pf_key)
467         else:
468             cpu_node = 0
469
470         return CpuUtils.cpu_range_per_node_str(
471             node, cpu_node, skip_cnt=cpu_skip_cnt, cpu_cnt=cpu_cnt,
472             smt_used=False)
473
474     @staticmethod
475     def get_affinity_vhost(
476             node, pf_key, skip_cnt=0, cpu_cnt=1):
477         """Get affinity for vhost. Result will be used to pin vhost threads.
478
479         :param node: Topology node.
480         :param pf_key: Topology interface.
481         :param skip_cnt: Amount of CPU cores to skip.
482         :param cpu_cnt: CPU threads count.
483         :type node: dict
484         :type pf_key: str
485         :type skip_cnt: int
486         :type cpu_cnt: int
487         :returns: List of CPUs allocated to vhost process.
488         :rtype: str
489         """
490         if pf_key:
491             cpu_node = Topology.get_interface_numa_node(node, pf_key)
492         else:
493             cpu_node = 0
494
495         smt_used = CpuUtils.is_smt_enabled(node[u"cpuinfo"])
496         if smt_used:
497             cpu_cnt = cpu_cnt // CpuUtils.NR_OF_THREADS
498
499         return CpuUtils.cpu_slice_of_list_per_node(
500             node, cpu_node=cpu_node, skip_cnt=skip_cnt, cpu_cnt=cpu_cnt,
501             smt_used=False)
502
503     @staticmethod
504     def get_cpu_idle_list(node, cpu_node, smt_used, cpu_alloc_str, sep=u","):
505         """
506         Get idle CPU List
507         :param node: Node dictionary with cpuinfo.
508         :param cpu_node: Numa node number.
509         :param smt_used: True - we want to use SMT, otherwise false.
510         :param cpu_alloc_str: vpp used cores.
511         :param sep: Separator, default: ",".
512         :type node: dict
513         :type cpu_node: int
514         :type smt_used: bool
515         :type cpu_alloc_str: str
516         :type smt_used: bool
517         :type sep: str
518         :rtype: list
519         """
520         cpu_list = CpuUtils.cpu_list_per_node(node, cpu_node, smt_used)
521         cpu_idle_list = [i for i in cpu_list
522                          if str(i) not in cpu_alloc_str.split(sep)]
523         return cpu_idle_list