summaryrefslogtreecommitdiff
path: root/tools/perf/python/net_dropmonitor.py
blob: fff637b5c7e900c6494aa8153c0673f297876b58 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
"""
Monitor the system for dropped packets and produce a report of drop locations and counts.
Ported from tools/perf/scripts/python/net_dropmonitor.py
"""
from __future__ import annotations

import argparse
from collections import defaultdict
import os
import sys
from typing import Tuple
import perf


class DropMonitor:
    """Monitors dropped packets and aggregates counts by location."""

    def __init__(self, kallsyms_path: str | None = None) -> None:
        self.drop_log: dict[int, int] = defaultdict(int)
        self.kallsyms: list[Tuple[int, str]] = []
        self.kallsyms_parsed = False
        self.resolved_syms: dict[int, Tuple[str, int]] = {}
        self.callchain_syms: dict[int, str] = {}
        self.kallsyms_path = (
            kallsyms_path
            or os.environ.get("PERF_SYMBOL_KALLSYMS")
            or "/proc/kallsyms"
        )

    def _parse_kallsyms(self) -> None:
        """Parse the kallsyms file and map kernel addresses to function symbols."""
        self.kallsyms.clear()
        self.kallsyms_parsed = True
        try:
            with open(self.kallsyms_path, "r", encoding="utf-8") as f:
                for line in f:
                    parts = line.split()
                    if len(parts) >= 3 and parts[1] in ('t', 'T', 'w', 'W'):
                        addr = int(parts[0], 16)
                        if addr > 0:
                            self.kallsyms.append((addr, parts[2]))
            self.kallsyms.sort(key=lambda x: x[0])
        except (FileNotFoundError, PermissionError):
            print(f"Failed to read {self.kallsyms_path}. Symbols will not be resolved.")

    def _get_sym(self, loc: int) -> Tuple[str, int]:
        """Resolve a memory location using session symbols or the kallsyms map."""
        # Priority order:
        # 1. Exact symbols with offsets resolved directly from the perf.data session
        #    (resolved_syms).
        # 2. Symbols captured from the sample's callchain in the trace file
        #    (callchain_syms) before falling back to self.kallsyms, so offline
        #    trace symbols are not overwritten by the live host's /proc/kallsyms.
        # 3. Binary search in self.kallsyms (from --kallsyms or /proc/kallsyms).
        if loc in self.resolved_syms:
            return self.resolved_syms[loc]
        if loc in self.callchain_syms:
            res = (self.callchain_syms[loc], 0)
            self.resolved_syms[loc] = res
            return res
        if not self.kallsyms:
            return f"{loc:#x}", 0

        start = 0
        end = len(self.kallsyms) - 1
        while start < end:
            mid = (start + end) // 2
            if self.kallsyms[mid][0] <= loc < self.kallsyms[mid+1][0]:
                start = mid
                break
            if loc < self.kallsyms[mid][0]:
                end = mid - 1
            else:
                start = mid + 1

        sym_addr, sym_name = self.kallsyms[start]
        if loc >= sym_addr:
            res = (sym_name, loc - sym_addr)
            self.resolved_syms[loc] = res
            return res
        return f"{loc:#x}", 0

    def print_drop_table(self) -> None:
        """Print aggregated results."""
        if not self.drop_log:
            print(f"{'LOCATION':>25} {'OFFSET':>25} {'COUNT':>25}")
            return

        if (not self.kallsyms_parsed
                and any(loc not in self.resolved_syms and loc not in self.callchain_syms
                        for loc in self.drop_log)):
            print("Gathering kallsyms data")
            self._parse_kallsyms()

        print(f"{'LOCATION':>25} {'OFFSET':>25} {'COUNT':>25}")
        sorted_keys = sorted(self.drop_log.keys())
        for sloc in sorted_keys:
            sym, off = self._get_sym(sloc)
            print(f"{sym:>25} {off:>25d} {self.drop_log[sloc]:>25d}")

    def process_event(self, sample: perf.sample_event) -> None:
        """Process a single sample event."""
        if "skb:kfree_skb" not in str(sample.evsel):
            return

        location = getattr(sample, "location", None)
        if location is not None:
            self.drop_log[location] += 1
            if location not in self.resolved_syms:
                sym = getattr(sample, "symbol", None)
                if getattr(sample, "sample_ip", 0) == location and sym and sym != "[unknown]":
                    self.resolved_syms[location] = (
                        sym,
                        getattr(sample, "sym_offset", 0) or 0,
                    )
                    self.callchain_syms.pop(location, None)
                else:
                    for entry in getattr(sample, "callchain", []) or []:
                        if isinstance(entry, dict):
                            entry_ip = entry.get("ip")
                            sym_info = entry.get("sym")
                            sym_name = sym_info.get("name") if isinstance(sym_info, dict) else None
                            sym_start = sym_info.get("start") if isinstance(sym_info, dict) else None
                            sym_off = (max(0, location - sym_start)
                                       if sym_start is not None else None)
                        else:
                            entry_ip = getattr(entry, "ip", None)
                            entry_sym = getattr(entry, "sym", None)
                            sym_name = (
                                getattr(entry_sym, "name", None)
                                or getattr(entry, "symbol", None)
                            )
                            sym_off = getattr(entry, "sym_offset", None)
                            if sym_off is None:
                                sym_start = getattr(entry_sym, "start", None)
                                if sym_start is not None:
                                    sym_off = max(0, location - sym_start)
                        if entry_ip == location and sym_name and sym_name != "[unknown]":
                            if sym_off is not None:
                                self.resolved_syms[location] = (sym_name, sym_off)
                                self.callchain_syms.pop(location, None)
                            else:
                                self.callchain_syms[location] = sym_name
                            break


if __name__ == "__main__":
    ap = argparse.ArgumentParser(
        description="Monitor the system for dropped packets and produce a "
                    "report of drop locations and counts.")
    ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
    ap.add_argument("-k", "--kallsyms", default=None,
                    help="Path to kallsyms file for offline symbol resolution")
    args = ap.parse_args()

    monitor = DropMonitor(kallsyms_path=args.kallsyms)
    session = None

    try:
        session = perf.session(perf.data(args.input), sample=monitor.process_event,
                               kallsyms=args.kallsyms)
        session.process_events()
    except KeyboardInterrupt:
        print("\nStopping trace...")
    except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e:
        print(f"Error processing events: {e}")
        sys.exit(1)
    finally:
        session = None

    monitor.print_drop_table()