xref: /dpdk/usertools/cpu_layout.py (revision 2b091dea3d37d076b6f11e6e676924be77122770)
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: BSD-3-Clause
3 # Copyright(c) 2010-2014 Intel Corporation
4 # Copyright(c) 2017 Cavium, Inc. All rights reserved.
5 
6 """Display CPU topology information."""
7 
8 import glob
9 import typing as T
10 
11 
12 def range_expand(rstr: str) -> T.List[int]:
13     """Expand a range string into a list of integers."""
14     # 0,1-3 => [0, 1-3]
15     ranges = rstr.split(",")
16     valset: T.List[int] = []
17     for r in ranges:
18         # 1-3 => [1, 2, 3]
19         if "-" in r:
20             start, end = r.split("-")
21             valset.extend(range(int(start), int(end) + 1))
22         else:
23             valset.append(int(r))
24     return valset
25 
26 
27 def read_sysfs(path: str) -> str:
28     """Read a sysfs file and return its contents."""
29     with open(path, encoding="utf-8") as fd:
30         return fd.read().strip()
31 
32 
33 def read_numa_node(base: str) -> int:
34     """Read the NUMA node of a CPU."""
35     node_glob = f"{base}/node*"
36     node_dirs = glob.glob(node_glob)
37     if not node_dirs:
38         return 0  # default to node 0
39     return int(node_dirs[0].split("node")[1])
40 
41 
42 def print_row(row: T.Tuple[str, ...], col_widths: T.List[int]) -> None:
43     """Print a row of a table with the given column widths."""
44     first, *rest = row
45     w_first, *w_rest = col_widths
46     first_end = " " * 4
47     rest_end = " " * 4
48 
49     print(first.ljust(w_first), end=first_end)
50     for cell, width in zip(rest, w_rest):
51         print(cell.rjust(width), end=rest_end)
52     print()
53 
54 
55 def print_section(heading: str) -> None:
56     """Print a section heading."""
57     sep = "=" * len(heading)
58     print(sep)
59     print(heading)
60     print(sep)
61     print()
62 
63 
64 def main() -> None:
65     """Print CPU topology information."""
66     sockets_s: T.Set[int] = set()
67     cores_s: T.Set[int] = set()
68     core_map: T.Dict[T.Tuple[int, int], T.List[int]] = {}
69     numa_map: T.Dict[int, int] = {}
70     base_path = "/sys/devices/system/cpu"
71 
72     cpus = range_expand(read_sysfs(f"{base_path}/online"))
73 
74     for cpu in cpus:
75         lcore_base = f"{base_path}/cpu{cpu}"
76         core = int(read_sysfs(f"{lcore_base}/topology/core_id"))
77         socket = int(read_sysfs(f"{lcore_base}/topology/physical_package_id"))
78         node = read_numa_node(lcore_base)
79 
80         cores_s.add(core)
81         sockets_s.add(socket)
82         key = (socket, core)
83         core_map.setdefault(key, [])
84         core_map[key].append(cpu)
85         numa_map[cpu] = node
86 
87     cores = sorted(cores_s)
88     sockets = sorted(sockets_s)
89 
90     print_section(f"Core and Socket Information (as reported by '{base_path}')")
91 
92     print("cores = ", cores)
93     print("sockets = ", sockets)
94     print("numa = ", sorted(set(numa_map.values())))
95     print()
96 
97     # Core, [NUMA, Socket, NUMA, Socket, ...]
98     heading_strs = "", *[v for s in sockets for v in ("", f"Socket {s}")]
99     sep_strs = tuple("-" * len(hstr) for hstr in heading_strs)
100     rows: T.List[T.Tuple[str, ...]] = []
101 
102     # track NUMA changes per socket
103     prev_numa: T.Dict[int, T.Optional[int]] = {socket: None for socket in sockets}
104     for c in cores:
105         # Core,
106         row: T.Tuple[str, ...] = (f"Core {c}",)
107 
108         # [NUMA, lcores, NUMA, lcores, ...]
109         for s in sockets:
110             try:
111                 lcores = core_map[(s, c)]
112 
113                 numa = numa_map[lcores[0]]
114                 numa_changed = prev_numa[s] != numa
115                 prev_numa[s] = numa
116 
117                 if numa_changed:
118                     row += (f"NUMA {numa}",)
119                 else:
120                     row += ("",)
121                 row += (str(lcores),)
122             except KeyError:
123                 row += ("", "")
124         rows += [row]
125 
126     # find max widths for each column, including header and rows
127     col_widths = [
128         max(len(tup[col_idx]) for tup in rows + [heading_strs])
129         for col_idx in range(len(heading_strs))
130     ]
131 
132     # print out table taking row widths into account
133     print_row(heading_strs, col_widths)
134     print_row(sep_strs, col_widths)
135     for row in rows:
136         print_row(row, col_widths)
137 
138 
139 if __name__ == "__main__":
140     main()
141