Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions drgn/commands/_builtin/crash/_runq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Copyright (c) 2025, Oracle and/or its affiliates.
# SPDX-License-Identifier: LGPL-2.1-or-later

"""
crash runq - Display the tasks on the run queues of each cpu.
Implements the crash "runq" command for drgn
"""
import argparse
from typing import Any, Dict, Iterator, List, Tuple

from drgn import Object, Program
from drgn.commands import argument, drgn_argument, mutually_exclusive_group
from drgn.commands.crash import crash_command, parse_cpuspec
from drgn.helpers.common.format import CellFormat, escape_ascii_string, print_table
from drgn.helpers.linux.cpumask import for_each_online_cpu
from drgn.helpers.linux.percpu import per_cpu
from drgn.helpers.linux.runqueue import rq_for_each_fair_task, rq_for_each_rt_task
from drgn.helpers.linux.sched import task_rq, task_since_last_arrival_ns


def get_rq_per_cpu(prog: Program, cpus: List[int] = []) -> Iterator[Tuple[int, Object]]:
"""
Get runqueue for selected cpus
:param prog: drgn program
:param cpus: a list of int
:return: Iterator of (int, ``struct rq``) tuples
"""
online_cpus = list(for_each_online_cpu(prog))

if cpus:
selected_cpus = [cpu for cpu in online_cpus if cpu in cpus]
else:
selected_cpus = online_cpus

for cpu in selected_cpus:
runqueue = per_cpu(prog["runqueues"], cpu)
yield (cpu, runqueue)


def timestamp_str(ns: int) -> str:
"""Convert nanoseconds to 'days HH:MM:SS.mmm' string."""
ms_total = ns // 1000000
secs_total, ms = divmod(ms_total, 1000)
mins_total, secs = divmod(secs_total, 60)
hours_total, mins = divmod(mins_total, 60)
days, hours = divmod(hours_total, 24)

return f"{days} {hours:02}:{mins:02}:{secs:02}.{ms:03}"


@crash_command(
description="Display the tasks on the run queues of each cpu.",
arguments=(
mutually_exclusive_group(
argument("-t", action="store_true", dest="show_timestamps"),
argument("-T", action="store_true", dest="show_lag"),
argument("-m", action="store_true", dest="pretty_runtime"),
argument("-g", action="store_true", dest="group"),
),
argument("-c", type=str, default="a", dest="cpus"),
Comment on lines +57 to +62
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These all need help strings.

drgn_argument,
),
)
def _crash_cmd_runq(
prog: Program, name: str, args: argparse.Namespace, **kwargs: Any
) -> None:
table_format = args.show_timestamps or args.show_lag or args.pretty_runtime
table: List[List[Any]] = []
headers: List[Any] = []

runq_clocks: Dict[int, int] = {}
cpus = parse_cpuspec(args.cpus).cpus(prog)
for i, (cpu, runqueue) in enumerate(get_rq_per_cpu(prog, cpus)):
curr_task = runqueue.curr[0].address_of_()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the [0].address_of_() for? Instead, I think we want to read it:

Suggested change
curr_task = runqueue.curr[0].address_of_()
curr_task = runqueue.curr.read_()

curr_task_addr = runqueue.curr.value_()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid re-reading runqueue.curr:

Suggested change
curr_task_addr = runqueue.curr.value_()
curr_task_addr = curr_task.value_()

(You could also remove this and use curr_task.value_() everywhere, but either way is fine.)

comm = escape_ascii_string(curr_task.comm.string_())
pid = curr_task.pid.value_()
prio = curr_task.prio.value_()
run_time = task_since_last_arrival_ns(curr_task)

# Show lag (skip formatting if not last CPU)
if args.show_lag:
runq_clocks[cpu] = task_rq(curr_task).clock.value_()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's kind of roundabout to go from runqueue -> current task -> runqueue. This also does unnecessary work at the beginning of the loop over CPUs. I think it'd make more sense to make the show_lag case a special case at the beginning of the function rather than doing it in this main loop. Something like:

    cpus = parse_cpuspec(args.cpus).cpus(prog)

    if args.show_lag:
        runq_clocks = {cpu: cpu_rq(cpu).clock.value_() for cpu in cpus}
        max_clock = max(runq_clocks.values())
        lags = [(cpu, max_clock - runq_clock) for cpu, runq_clock in runq_clocks.items()]
        lags.sort(key=operator.itemgetter(1))
        for cpu, lag in lags:
            print(f"  CPU {cpu}: {lag / 1e9:.2f} secs")
        return

    for i, (cpu, runqueue) in enumerate(get_rq_per_cpu(prog, cpus)):
        ...

if i == len(cpus) - 1:
max_clock = max(runq_clocks.values())
lags = {
c: max_clock - runq_clock for c, runq_clock in runq_clocks.items()
}
sorted_lags = sorted(lags.items(), key=lambda item: item[1])
for c, lag in sorted_lags:
print(f"CPU {c}: {lag / 1e9:.2f} secs")
return
else:
continue

if table_format:
row = [
Copy link
Owner

@osandov osandov Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format of these options is fairly different from the crash output. Was that an intentional choice? That's okay if there's an intentional reason, but it'd be nice to imitate crash's format if not.

CellFormat(cpu, ">"),
CellFormat(pid, ">"),
CellFormat(curr_task_addr, "x"),
CellFormat(prio, ">"),
CellFormat(comm, "<"),
]
if args.pretty_runtime:
row.append(CellFormat(timestamp_str(run_time), ">"))

if args.show_timestamps:
rq_ts = runqueue.clock.value_()
task_ts = curr_task.sched_info.last_arrival.value_()
row += [
CellFormat(f"{rq_ts:013d}", ">"),
CellFormat(f"{task_ts:013d}", "<"),
]
table.append(row)
else:
print(f"CPU {cpu} RUNQUEUE: {hex(runqueue.address_of_().value_())}")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crash doesn't include the 0x prefix. Also, a shorter way to do .address_of_().value_() is .address_.

Suggested change
print(f"CPU {cpu} RUNQUEUE: {hex(runqueue.address_of_().value_())}")
print(f"CPU {cpu} RUNQUEUE: {runqueue.address_:x}")

The same hex() -> ":x" format thing applies in a few more places, too.

print(
f' CURRENT: PID: {pid:<6d} TASK: {hex(curr_task_addr)} PRIO: {prio} COMMAND: "{comm}"'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extra spaces after "CURRENT:" make it look like there's supposed to be a value there but it's empty. Crash also doesn't include them.

Suggested change
f' CURRENT: PID: {pid:<6d} TASK: {hex(curr_task_addr)} PRIO: {prio} COMMAND: "{comm}"'
f' CURRENT: PID: {pid:<6d} TASK: {hex(curr_task_addr)} PRIO: {prio} COMMAND: "{comm}"'

)

rt_tasks = list(rq_for_each_rt_task(runqueue))
cfs_tasks = list(rq_for_each_fair_task(runqueue))

if args.group:
root_task_group_addr = prog["root_task_group"].address_of_().value_()
if rt_tasks:
print(
f" ROOT_TASK_GROUP: {hex(root_task_group_addr)} RT_RQ: {hex(runqueue.rt.address_of_().value_())}"
)
if cfs_tasks:
print(
f" ROOT_TASK_GROUP: {hex(root_task_group_addr)} CFS_RQ: {hex(runqueue.cfs.address_of_().value_())}"
)
if cfs_tasks or rt_tasks:
print(
" " * 4,
f'[{prio:3d}] PID: {pid:<6d} TASK: {hex(curr_task_addr)} COMMAND: "{comm}" [CURRENT]',
)
Comment on lines +126 to +140
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the crash implementation actually walks up the task group hierarchy, which this doesn't appear to do. Maybe let's just drop -g for now so we can land the more basic stuff, first.


# RT runqueue
prio_array = runqueue.rt.active.address_of_()
print(f" RT PRIO_ARRAY: {hex(prio_array)}")
is_rt_queue = False
if rt_tasks:
for task in rt_tasks:
if task == runqueue.curr:
continue
is_rt_queue = True
print(
" " * 4,
f"[{task.prio.value_():3d}] PID: {task.pid.value_():<6d} TASK: {hex(int(task))} "
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spacing is inconsistent with crash here:

Suggested change
f"[{task.prio.value_():3d}] PID: {task.pid.value_():<6d} TASK: {hex(int(task))} "
f"[{task.prio.value_():3d}] PID: {task.pid.value_():<6d} TASK: {hex(int(task))} "

f'COMMAND: "{escape_ascii_string(task.comm.string_())}"',
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight shortcut that also takes care of escaping double quotes if applicable:

Suggested change
f'COMMAND: "{escape_ascii_string(task.comm.string_())}"',
f"COMMAND: {double_quote_ascii_string(task.comm.string_())}",

)
if not is_rt_queue:
print(" [no tasks queued]")

# CFS runqueue
cfs_root = runqueue.cfs.tasks_timeline.address_of_().value_()
print(f" CFS RB_ROOT: {hex(cfs_root)}")
is_cfs_queue = False
if cfs_tasks:
for task in cfs_tasks:
if task == runqueue.curr:
continue
is_cfs_queue = True
print(
" " * 4,
f"[{task.prio.value_():3d}] PID: {task.pid.value_():<6d} TASK: {hex(int(task))} "
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f"[{task.prio.value_():3d}] PID: {task.pid.value_():<6d} TASK: {hex(int(task))} "
f"[{task.prio.value_():3d}] PID: {task.pid.value_():<6d} TASK: {hex(int(task))} "

f'COMMAND: "{escape_ascii_string(task.comm.string_())}"',
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f'COMMAND: "{escape_ascii_string(task.comm.string_())}"',
f"COMMAND: {double_quote_ascii_string(task.comm.string_())}",

)
if not is_cfs_queue:
print(" [no tasks queued]")
print()

if table_format:
headers = [
CellFormat("CPU", "<"),
CellFormat("PID", "<"),
CellFormat("TASK", "<"),
CellFormat("PRIO", "<"),
CellFormat("COMMAND", "<"),
]
if args.pretty_runtime:
headers.append(CellFormat("RUNTIME", "<"))
if args.show_timestamps:
headers += [
CellFormat("RQ_TIMESTAMP", "<"),
CellFormat("TASK_TIMESTAMP", "<"),
]
print_table([headers] + table)
41 changes: 41 additions & 0 deletions drgn/helpers/linux/runqueue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright (c) 2025, Oracle and/or its affiliates.
# SPDX-License-Identifier: LGPL-2.1-or-later

"""
Runqueue
--------

The ``drgn.helpers.linux.runqueue`` module provides helpers for working with the
Linux runqueue.
"""

from typing import Iterator

from drgn import Object
from drgn.helpers.linux.list import list_for_each_entry


def rq_for_each_rt_task(runqueue: Object) -> Iterator[Object]:
"""
Get real-time runqueue tasks in real-time scheduler.

:param runqueue: ``struct rq *``
:return: Iterator of ``struct task_struct``
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually yields pointers, right?

Suggested change
:return: Iterator of ``struct task_struct``
:return: Iterator of ``struct task_struct *``

Copy link
Owner

@osandov osandov Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be updated to "Iterator of struct task_struct * objects."

"""
rt_prio_array = runqueue.rt.active.queue
for que in rt_prio_array:
yield from list_for_each_entry(
"struct task_struct", que.address_of_(), "rt.run_list"
)


def rq_for_each_fair_task(runqueue: Object) -> Iterator[Object]:
"""
Get CFS runqueue tasks in cfs scheduler.

:param runqueue: ``struct rq *``
:return: Iterator of (``struct task_struct``, int) tuples
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems out of date.

Suggested change
:return: Iterator of (``struct task_struct``, int) tuples
:return: Iterator of ``struct task_struct *`` objects.

"""
return list_for_each_entry(
"struct task_struct", runqueue.cfs_tasks.address_of_(), "se.group_node"
)
Loading