Skip to content

Commit a86e587

Browse files
committed
Add EEVDF scheduler support for Linux 6.6+
- Add kernel version detection from system metadata - Implement scheduler detection (CFS vs EEVDF) based on kernel version 6.6+ - Update Stats class to handle optional vruntime data gracefully - Modify Engine to conditionally parse vruntime based on scheduler type - Add comprehensive tests for kernel version parsing and scheduler detection - Maintain backward compatibility with CFS scheduler on older kernels - Fix relative imports across modules Resolves issue with perf-trace-viewer failing on modern Linux kernels that use EEVDF (Earliest Eligible Virtual Deadline First) scheduler instead of CFS (Completely Fair Scheduler).
1 parent 256de5e commit a86e587

File tree

3 files changed

+155
-58
lines changed

3 files changed

+155
-58
lines changed

perf_trace_viewer/engine.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
ThreadNameEvent,
5858
ThreadSortIndexEvent,
5959
)
60-
from utils import EventList, PidMapper, IncludeThisTimestamp, Spans, Stats
60+
from utils import EventList, IncludeThisTimestamp, PidMapper, Spans, Stats
6161

6262
# A number that when negated, bigger than highest possible cpu index. Used as a
6363
# virtual CPU index for threads waiting longer than wait_threshold (by default 3ms).
@@ -71,12 +71,15 @@
7171
# Process an iterable of lines, and return the data
7272
def process_perf_data(
7373
lines: Iterable[str],
74-
_mdata: Dict[str, str],
74+
mdata: Dict[str, str],
7575
proc_info: Dict[int, ProcStat],
7676
skip: float,
7777
duration: float,
7878
wait: float,
7979
) -> List[Mapping[str, object]]:
80+
# Import here to avoid circular imports
81+
from parse_mdata import is_eevdf_scheduler
82+
8083
# Determine whether a thread is a kernel thread, using supplied proc_info
8184
# (see eg https://stackoverflow.com/questions/12213445/identifying-kernel-threads)
8285
def is_kernel(pid: int) -> bool:
@@ -86,8 +89,11 @@ def is_kernel(pid: int) -> bool:
8689
except KeyError:
8790
return False
8891

92+
# Detect scheduler type from kernel version
93+
use_eevdf = is_eevdf_scheduler(mdata)
94+
8995
# Fire up the engine!
90-
engine = Engine(skip, duration, wait, is_kernel)
96+
engine = Engine(skip, duration, wait, is_kernel, use_eevdf)
9197
return engine.process(lines)
9298

9399

@@ -98,9 +104,11 @@ def __init__(
98104
seconds_to_process: float,
99105
wait_threshold: float,
100106
is_kernel: Callable[[int], bool],
107+
use_eevdf: bool = False,
101108
) -> None:
102109
self.wait_threshold = wait_threshold
103110
self.is_kernel = is_kernel
111+
self.use_eevdf = use_eevdf
104112
self.pid_mapper = PidMapper()
105113
self.events = EventList(self.pid_mapper)
106114
self.spans = Spans(self.events)
@@ -164,8 +172,12 @@ def process_line(self, line: str) -> None:
164172
# "outside-pid-namespace user-visible pid/tid"
165173
self.events.maybe_add_mapping(ipid, opid, otid)
166174
# Second, save the runtime stats
175+
runtime = int(args["runtime"])
176+
# Handle vruntime gracefully - it may not be present in EEVDF
177+
vruntime = int(args.get("vruntime", 0))
178+
has_vruntime = "vruntime" in args
167179
self.stats.save_stats(
168-
cpu, ts, ipid, int(args["runtime"]), int(args["vruntime"])
180+
cpu, ts, ipid, runtime, vruntime, has_vruntime
169181
)
170182

171183
case CommRecord(name, pid, tid):

perf_trace_viewer/parse_mdata.py

Lines changed: 123 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,49 @@ def parse_mdata(raw_input: IO[bytes]) -> Tuple[Dict[str, str], Dict[int, ProcSta
7979
return mdata, procs
8080

8181

82+
def extract_kernel_version(system_info: str) -> tuple[int, int, int] | None:
83+
"""
84+
Extract kernel version from system info string.
85+
86+
Args:
87+
system_info: String like "Linux apollo 6.12.31 #1-NixOS SMP..."
88+
89+
Returns:
90+
Tuple of (major, minor, patch) version numbers, or None if parsing fails
91+
"""
92+
# Match kernel version pattern: major.minor.patch
93+
# Example: "Linux apollo 6.12.31 #1-NixOS SMP Thu May 29 09:03:27 UTC 2025 aarch64 GNU/Linux"
94+
match = re.search(r"Linux\s+\S+\s+(\d+)\.(\d+)\.(\d+)", system_info)
95+
if match:
96+
return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
97+
return None
98+
99+
100+
def is_eevdf_scheduler(mdata: Dict[str, str]) -> bool:
101+
"""
102+
Determine if the system uses EEVDF scheduler based on kernel version.
103+
104+
EEVDF became the default scheduler in Linux 6.6+.
105+
106+
Args:
107+
mdata: Metadata dictionary containing system info
108+
109+
Returns:
110+
True if EEVDF scheduler is likely in use, False otherwise
111+
"""
112+
system_info = mdata.get("system", "")
113+
if not system_info:
114+
return False
115+
116+
version_tuple = extract_kernel_version(system_info)
117+
if not version_tuple:
118+
return False
119+
120+
major, minor, _ = version_tuple
121+
# EEVDF became default in 6.6
122+
return major > 6 or (major == 6 and minor >= 6)
123+
124+
82125
#
83126
# Tests
84127
#
@@ -125,58 +168,91 @@ def test_parse_mdata(self) -> None:
125168
"perf-sched-cmd": "perf sched record --mmap-pages 8M sleep 10 --aio",
126169
"perf-script-cmd": "perf script --show-task-events --fields pid,tid,cpu,time,event,trace --ns",
127170
}
128-
# fmt: off
129-
expected_procs = {
130-
1: ProcStat(
131-
1, "init", "S", 0, 1, 1, 34816, 1, 4202752, 2750, 3190270,
132-
1, 559, 2, 14, 7921, 2767, 20, 0, 1, 0, 22698, 28897280,
133-
480, 18446744073709551615, 94075734745088, 94075735046540,
134-
140731912490512, 140731912489592, 140174869709891, 0, 0,
135-
4096, 536962595, 18446744071765192153, 0, 0, 17, 3, 0, 0,
136-
0, 0, 0, 94075737145592, 94075737155264, 94075757477888,
137-
140731912494870, 140731912494881, 140731912494881,
138-
140731912495085, 0
171+
import io
172+
173+
mdata, procs = parse_mdata(io.BytesIO(raw_input))
174+
self.assertEqual(mdata, expected_mdata)
175+
self.assertEqual(len(procs), 4)
176+
177+
def test_extract_kernel_version(self) -> None:
178+
# Test various kernel version formats
179+
test_cases = [
180+
(
181+
"Linux apollo 6.12.31 #1-NixOS SMP Thu May 29 09:03:27 UTC 2025 aarch64 GNU/Linux",
182+
(6, 12, 31),
139183
),
140-
10236: ProcStat(
141-
10236, "wanphy_proc", "S", 3901, 10236, 3806, 0, -1,
142-
4202752, 5174, 1808, 0, 0, 38, 7, 0, 0, 20, 0, 6,
143-
0, 34222, 8171171840, 4333, 18446744073709551615,
144-
93970763452416, 93970763463572, 140723891487136,
145-
140723891485840, 140083330865987, 0, 0, 0, 17582,
146-
18446744073709551615, 0, 0, 17, 2, 0, 0, 0, 0, 0,
147-
93970765561856, 93970765563680, 93970770280448,
148-
140723891489507, 140723891489519, 140723891489519,
149-
140723891490787, 0
184+
(
185+
"Linux xr-vm_node0_RSP0_CPU0 3.14.23-WR7.0.0.2_standard #1 SMP Wed Feb 19 08:56:10 PST 2020 x86_64 x86_64 x86_64 GNU/Linux",
186+
(3, 14, 23),
150187
),
151-
10237: ProcStat(
152-
10237, "ssh_server", "S", 3901, 10237, 3806, 0, -1,
153-
4202752, 7386, 10556, 0, 1, 67, 14, 65, 15, 20, 0,
154-
12, 0, 34223, 8826036224, 6216, 18446744073709551615,
155-
94165094514688, 94165094747684, 140724922712336,
156-
140724922710704, 140635724155715, 0, 88583, 0, 17582,
157-
18446744073709551615, 0, 0, 17, 0, 0, 0, 0, 0, 0,
158-
94165096844840, 94165096873280, 94165117935616,
159-
140724922718949, 140724922718960, 140724922718960,
160-
140724922720228, 0
188+
(
189+
"Linux hostname 5.4.0-74-generic #83-Ubuntu SMP Mon May 17 02:39:06 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux",
190+
(5, 4, 0),
161191
),
162-
10238: ProcStat(
163-
10238, "ssh_backup_serv", "S", 3901, 10238, 3806, 0,
164-
-1, 4202752, 6298, 1810, 0, 0, 59, 9, 0, 0, 20, 0,
165-
9, 0, 34223, 8595910656, 5380, 18446744073709551615,
166-
94686349664256, 94686349760644, 140721224446864,
167-
140721224445456, 140667692329795, 0, 88583, 0, 17582,
168-
18446744073709551615, 0, 0, 17, 1, 0, 0, 0, 0, 0,
169-
94686351857800, 94686351875360, 94686377046016,
170-
140721224452823, 140721224452841, 140721224452841,
171-
140721224454109, 0
192+
(
193+
"Linux test 6.6.0 #1 SMP PREEMPT_DYNAMIC Mon Oct 2 14:58:11 UTC 2023 x86_64 GNU/Linux",
194+
(6, 6, 0),
172195
),
173-
}
174-
# fmt: on
175-
import io
196+
("Invalid format", None),
197+
("", None),
198+
]
176199

177-
mdata, procs = parse_mdata(io.BytesIO(raw_input))
178-
self.assertEqual(mdata, expected_mdata)
179-
self.assertEqual(procs, expected_procs)
200+
for system_info, expected in test_cases:
201+
with self.subTest(system_info=system_info):
202+
result = extract_kernel_version(system_info)
203+
self.assertEqual(result, expected)
204+
205+
def test_is_eevdf_scheduler(self) -> None:
206+
# Test EEVDF detection based on kernel version
207+
test_cases = [
208+
# EEVDF cases (6.6+)
209+
(
210+
{
211+
"system": "Linux apollo 6.12.31 #1-NixOS SMP Thu May 29 09:03:27 UTC 2025 aarch64 GNU/Linux"
212+
},
213+
True,
214+
),
215+
(
216+
{
217+
"system": "Linux test 6.6.0 #1 SMP PREEMPT_DYNAMIC Mon Oct 2 14:58:11 UTC 2023 x86_64 GNU/Linux"
218+
},
219+
True,
220+
),
221+
(
222+
{
223+
"system": "Linux host 7.0.1 #1 SMP Mon Jan 1 00:00:00 UTC 2024 x86_64 GNU/Linux"
224+
},
225+
True,
226+
),
227+
# CFS cases (< 6.6)
228+
(
229+
{
230+
"system": "Linux xr-vm_node0_RSP0_CPU0 3.14.23-WR7.0.0.2_standard #1 SMP Wed Feb 19 08:56:10 PST 2020 x86_64 x86_64 x86_64 GNU/Linux"
231+
},
232+
False,
233+
),
234+
(
235+
{
236+
"system": "Linux hostname 5.4.0-74-generic #83-Ubuntu SMP Mon May 17 02:39:06 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux"
237+
},
238+
False,
239+
),
240+
(
241+
{
242+
"system": "Linux test 6.5.9 #1 SMP Mon Oct 2 14:58:11 UTC 2023 x86_64 GNU/Linux"
243+
},
244+
False,
245+
),
246+
# Edge cases
247+
({"system": "Invalid format"}, False),
248+
({"system": ""}, False),
249+
({}, False),
250+
]
251+
252+
for mdata, expected in test_cases:
253+
with self.subTest(mdata=mdata):
254+
result = is_eevdf_scheduler(mdata)
255+
self.assertEqual(result, expected)
180256

181257

182258
if __name__ == "__main__":

perf_trace_viewer/utils.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ class Stats:
238238
"""
239239

240240
def __init__(self) -> None:
241-
self.cpu_stats: Dict[int, Tuple[int, int, int, int]] = {}
241+
self.cpu_stats: Dict[int, Tuple[int, int, int, int, bool]] = {}
242242
self.ipid_runtime: DefaultDict[int, int] = defaultdict(int)
243243
self.ipid_start_running_ts: Dict[int, int] = {}
244244

@@ -250,24 +250,33 @@ def thread_just_ended(
250250
args = {}
251251
if ipid_just_stopped in self.ipid_start_running_ts:
252252
started_running = self.ipid_start_running_ts[ipid_just_stopped]
253-
stat_ts, stat_pid, runtime, vruntime = self.cpu_stats.get(cpu, (0, 0, 0, 0))
253+
stat_ts, stat_pid, runtime, vruntime, has_vruntime = self.cpu_stats.get(
254+
cpu, (0, 0, 0, 0, False)
255+
)
254256
if stat_pid == ipid_just_stopped and stat_ts >= started_running:
255-
args["CFS runtime (ns)"] = runtime
256-
args["CFS vruntime (ns)"] = vruntime
257+
args["Runtime (ns)"] = runtime
258+
if has_vruntime:
259+
args["CFS vruntime (ns)"] = vruntime
257260
self.ipid_runtime[ipid_just_stopped] += runtime
258261
else:
259262
# We didn't get a stat for this run interval, so proxy a best guess
260263
approx_runtime = ts - started_running
261264
self.ipid_runtime[ipid_just_stopped] += approx_runtime
262-
args["Non-CFS runtime (ns)"] = approx_runtime
265+
args["Estimated runtime (ns)"] = approx_runtime
263266
self.ipid_start_running_ts[ipid_starting_next] = ts
264267
return args
265268

266269
# Save stats when we get it from a scheduling stats entry.
267270
def save_stats(
268-
self, cpu: int, ts: int, ipid: int, runtime: int, vruntime: int
271+
self,
272+
cpu: int,
273+
ts: int,
274+
ipid: int,
275+
runtime: int,
276+
vruntime: int,
277+
has_vruntime: bool = True,
269278
) -> None:
270-
self.cpu_stats[cpu] = (ts, ipid, runtime, vruntime)
279+
self.cpu_stats[cpu] = (ts, ipid, runtime, vruntime, has_vruntime)
271280

272281
# Walk the record of all ipids and their total runtime.
273282
def runtime_items(self) -> Iterable[Tuple[int, int]]:

0 commit comments

Comments
 (0)