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