blob: 92d4aaea3c716f333e45c76eff601a49c6f590a7 [file]
#!/usr/bin/env drgn
#
# Copyright (C) 2013-2026 Frederic Weisbecker
# Copyright (C) 2024-2026 Marco Crivellari
# SPDX-License-Identifier: GPL-3.0-or-later
"""
This is a drgn (dragon) Python script.
For info visit: https://github.com/osandov/drgn
The script produce a table showing the actual vectors allocated and used,
and how much free space there is. If CPU Isolation is used and IRQs are
on one of the isolated CPUs, it prints also IRQ names.
E.g.
# ./dump_irq_on_hk.py --ignore-housekeeping
=== Global IRQ Vector Matrix Stats ===
Matrix Bits : 256
Vectors per CPU : 204 (Usable limit)
Global Available : 37173
Global Reserved : 516
Total Allocated : 1261
=== CPU Vector Occupancy ===
CPU | ROLE | ALLOCATED | AVAILABLE | MANAGED
------------------------------------------------------
5 | Isolated | 0 | 201 | 0
7 | Isolated | 0 | 201 | 0
12 | Isolated | 8 | 193 | 0
=== IRQs effectively targeting isolated CPUs ===
CPU | IRQ | NAME
----------------------------------------
12 | 340 | qat0-bundle12
12 | 714 | qat1-bundle15
12 | 721 | qat1-bundle22
12 | 902 | qat2-bundle2
12 | 919 | qat2-bundle19
12 | 947 | qat2-bundle47
12 | 963 | qat2-bundle63
12 | 977 | qat3-bundle12
--ignore-housekeeping
This flags ignore housekeeping CPUs (shows only isolated)
--ignore-isolated
If specified, ignore isolated CPUs (shows only HK)
"""
import drgn
import argparse
from drgn import cast
from drgn.helpers.linux.cpumask import for_each_cpu
from drgn.helpers.linux.percpu import per_cpu_ptr
from drgn.helpers.linux.mapletree import mt_for_each
from drgn.helpers.linux.radixtree import radix_tree_for_each
# -------------------------------------------------------------------------
# DRGN Struct Helpers
# -------------------------------------------------------------------------
def get_val(obj, attr, default=0):
"""Safely fetch a value from a drgn object, returning a default if missing."""
try:
if hasattr(obj, attr):
val = getattr(obj, attr)
return val.value_() if val else default
except (AttributeError, drgn.FaultError, TypeError):
pass
return default
# -------------------------------------------------------------------------
# IRQ Iteration & Resolution
# -------------------------------------------------------------------------
# for_each_irq_desc and irq_desc_action_names taken from drgn Github repo
def for_each_irq_desc():
"""Iterate over all allocated interrupt descriptors (supports v6.5+ Maple Tree)."""
irq_desc_type = prog.type("struct irq_desc *")
try:
# Kernel 6.5+ (Maple Tree)
sparse_irqs = prog["sparse_irqs"]
for irq, _, entry in mt_for_each(sparse_irqs.address_of_()):
yield irq, cast(irq_desc_type, entry)
return
except drgn.ObjectNotFoundError:
pass
# Kernel < 6.5 (Radix Tree)
for irq, entry in radix_tree_for_each(prog["irq_desc_tree"].address_of_()):
yield irq, cast(irq_desc_type, entry)
def irq_desc_action_names(desc):
"""Get a list of action names set on an interrupt descriptor."""
action = desc.action
names = []
while action:
name = action.name
if name:
names.append(name.string_().decode('utf-8', 'ignore'))
action = action.next
return names
def irq_effective_cpus(desc):
"""CPUs this IRQ is effectively targeting."""
common = desc.irq_common_data
mask = common.effective_affinity if hasattr(common, "effective_affinity") else common.affinity
return set(for_each_cpu(as_cpumask_ptr(mask)))
def irq_name(desc):
names = irq_desc_action_names(desc)
if names:
return ", ".join(names)
if desc.name:
return desc.name.string_().decode('utf-8', 'ignore')
return "(none)"
def collect_isolated_irqs(isolated_cpus):
by_cpu = {cpu: [] for cpu in isolated_cpus}
for irq, desc in for_each_irq_desc():
try:
cpus = irq_effective_cpus(desc)
except (AttributeError, drgn.FaultError):
continue
# IRQ touches any isolated cpus?
hit = cpus & isolated_cpus
if not hit:
continue
name = irq_name(desc)
for cpu in hit:
by_cpu[cpu].append((irq, name))
return by_cpu
# -------------------------------------------------------------------------
# CPUMask Helpers
# -------------------------------------------------------------------------
def as_cpumask_ptr(mask):
try:
return mask[0].address_of_()
except TypeError:
return mask.address_of_()
def get_isolated_cpus():
try:
mask_ptr = as_cpumask_ptr(prog["tick_nohz_full_mask"])
if mask_ptr.value_() == 0:
return set()
return set(for_each_cpu(mask_ptr))
except (KeyError, drgn.FaultError):
return set()
def get_online_cpus():
try:
mask_ptr = as_cpumask_ptr(prog["__cpu_online_mask"])
if mask_ptr.value_() == 0:
raise KeyError("Online mask pointer is NULL")
return list(for_each_cpu(mask_ptr))
except (KeyError, drgn.FaultError):
try:
return list(range(prog["nr_cpu_ids"].value_()))
except KeyError:
return list(range(128))
# -------------------------------------------------------------------------
# Formatting and Output
# -------------------------------------------------------------------------
def print_global_stats(vm):
print("=== Global IRQ Vector Matrix Stats ===")
print(f" Matrix Bits : {get_val(vm, 'matrix_bits')}")
print(f" Vectors per CPU : {get_val(vm, 'alloc_size')} (Usable limit)")
print(f" Global Available : {get_val(vm, 'global_available')}")
print(f" Global Reserved : {get_val(vm, 'global_reserved')}")
print(f" Total Allocated : {get_val(vm, 'total_allocated')}\n")
def print_isolated_irqs(isolated_cpus):
print("=== IRQs effectively targeting isolated CPUs ===")
if not isolated_cpus:
print("(no isolated CPUs found)\n")
return
by_cpu = collect_isolated_irqs(isolated_cpus)
print(f"{'CPU':<5} | {'IRQ':<6} | {'NAME'}")
print("-" * 40)
found = False
for cpu in sorted(by_cpu):
for irq, name in sorted(by_cpu[cpu]):
found = True
print(f"{cpu:<5} | {irq:<6} | {name}")
if not found:
print("(none)")
print()
def dump_irq_matrix(args):
try:
vm = prog["vector_matrix"][0]
except KeyError:
print("Error: 'vector_matrix' not found. x86 APIC matrix allocator required.")
return
print_global_stats(vm)
online_cpus = get_online_cpus()
isolated_cpus = get_isolated_cpus()
# Filter target CPUs based on CLI flags
target_cpus = [
cpu for cpu in online_cpus
if not (cpu in isolated_cpus and args.ignore_isolated) and
not (cpu not in isolated_cpus and args.ignore_housekeeping)
]
print("=== CPU Vector Occupancy ===")
print(f"{'CPU':<5} | {'ROLE':<12} | {'ALLOCATED':<10} | {'AVAILABLE':<10} | {'MANAGED':<10}")
print("-" * 57)
for cpu in target_cpus:
role = "Isolated" if cpu in isolated_cpus else "Housekeeping"
try:
cmap = per_cpu_ptr(vm.maps, cpu)[0]
except (drgn.FaultError, TypeError):
continue
if get_val(cmap, 'online', default=1) == 0:
continue
alloc_val = get_val(cmap, 'allocated')
avail_val = get_val(cmap, 'available')
man_val = get_val(cmap, 'managed')
warn = " <--- [WARNING: EXHAUSTION RISK]" if avail_val < 10 else ""
print(f"{cpu:<5} | {role:<12} | {alloc_val:<10} | {avail_val:<10} | {man_val:<10}{warn}")
print()
if not args.ignore_isolated:
print_isolated_irqs(isolated_cpus)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Dump x86 IRQ Vector Matrix Occupancy & Names")
parser.add_argument("--ignore-housekeeping", action="store_true", help="Hide Housekeeping CPUs from output")
parser.add_argument("--ignore-isolated", action="store_true", help="Hide Isolated CPUs from output")
args = parser.parse_args()
dump_irq_matrix(args)