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
8import glob
9import typing as T
10
11
12def 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
27def 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
33def 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
42def 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
55def 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
64def 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
139if __name__ == "__main__":
140    main()
141