#!/usr/libexec/platform-python
#
# nicrx_drop  Network RX drop analysis tool
#       For Linux, uses BCC, eBPF.
#
# This tool analyzes network RX drop by monitoring:
# 1. Packet drop detection: monitor /proc/net/dev for rx drop
# 2. Drop stack trace: capture kfree_skb stack traces in RX context
# 3. Memory allocation failure: capture alloc failure stack traces in RX context
# 4. NIC queue check: capture ethtool information

from __future__ import print_function
from bcc import BPF
import argparse
import time
import subprocess
import re
import gc
from collections import defaultdict
from datetime import datetime

def read_proc_net_dev():
    """Read /proc/net/dev and return interface names and rx_drop stats"""
    interfaces = []
    stats = {}
    try:
        with open('/proc/net/dev', 'r') as f:
            for line in f:
                line = line.strip()
                if ':' in line:
                    parts = line.split(':')
                    ifname = parts[0].strip()
                    interfaces.append(ifname)
                    data = parts[1].split()
                    if len(data) >= 4:
                        stats[ifname] = int(data[3])
    except:
        pass
    return interfaces, stats

def filter_existing_interfaces(requested_interfaces):
    """Filter out interfaces that don't exist"""
    available, _ = read_proc_net_dev()
    existing = [iface for iface in requested_interfaces if iface in available]
    missing = [iface for iface in requested_interfaces if iface not in available]
    return existing, missing

# arguments
examples = """examples:
    ./netrx_drop.py                    # monitor eth0 and eth1, 1 iteration, 10 seconds
    ./netrx_drop.py -i eth0            # monitor eth0 only
    ./netrx_drop.py -i eth0 -c 3       # 3 iterations
    ./netrx_drop.py -i eth0 -d 30      # 30 seconds per iteration
    ./netrx_drop.py -i eth0 -c 2 -d 20 # 2 iterations, 20 seconds each
"""
parser = argparse.ArgumentParser(
    description="Network RX drop analysis",
    formatter_class=argparse.RawDescriptionHelpFormatter,
    epilog=examples)
parser.add_argument("-i", "--interface", 
                    help="network interface(s), comma separated (default: eth0,eth1)")
parser.add_argument("-c", "--count", type=int, default=1,
                    help="number of iterations (default: 1. max: 20)")
parser.add_argument("-d", "--duration", type=int, default=10,
                    help="duration per iteration in seconds (default: 10 seconds. max: 60 seconds )")
parser.add_argument("--ebpf", action="store_true",
                    help=argparse.SUPPRESS)
args = parser.parse_args()

# Limit duration to maximum 60 seconds
if args.duration > 60:
    print("Warning: Duration exceeds maximum of 60 seconds. Setting to 60 seconds.")
    args.duration = 60

# Limit count to maximum 20 iterations
if args.count > 20:
    print("Warning: Count exceeds maximum of 20 iterations. Setting to 20.")
    args.count = 20

# parse interfaces
if args.interface:
    requested_interfaces = [iface.strip() for iface in args.interface.split(',')]
else:
    requested_interfaces = ['eth0', 'eth1']

# Filter out interfaces that don't exist
interfaces, missing_interfaces = filter_existing_interfaces(requested_interfaces)

if missing_interfaces:
    print("Warning: The following interfaces do not exist and will be skipped: %s" % ', '.join(missing_interfaces))

if not interfaces:
    print("Error: No valid interfaces found. Available interfaces:")
    available, _ = read_proc_net_dev()
    if available:
        print("  %s" % ', '.join(available))
    else:
        print("  (none)")
    exit(1)

# BPF program for kfree_skb stack trace
bpf_text_kfree_skb = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>

struct kfree_skb_event {
    u64 timestamp;
    u32 cpu;
    u32 reason;
    u32 stack_id;
    u32 pid;
    char comm[TASK_COMM_LEN];
};

BPF_STACK_TRACE(stack_traces_kfree, 1024);
BPF_PERF_OUTPUT(events_kfree);

// Use tracepoint for kfree_skb - more stable than kprobe
// Define enum to avoid incomplete type error
enum skb_drop_reason {
    SKB_DROP_REASON_NOT_SPECIFIED = 0,
    SKB_DROP_REASON_NO_SOCKET,
    SKB_DROP_REASON_PKT_TOO_SMALL,
    // Add more as needed, but we only care about the reason value
};

// Task flags for context detection
#define PF_IN_SOFTIRQ 0x00000200

TRACEPOINT_PROBE(skb, kfree_skb) {
    // Optimization: Filter in kernel space - only process events in softirq context
    // RX path typically runs in softirq (NET_RX_SOFTIRQ), this reduces user-space overhead
    struct task_struct *task = (struct task_struct *)bpf_get_current_task();
    unsigned int flags = 0;
    bpf_probe_read_kernel(&flags, sizeof(flags), &task->flags);
    
    // Skip if not in softirq context (not RX path)
    if (!(flags & PF_IN_SOFTIRQ)) {
        return 0;
    }
    
    struct kfree_skb_event event = {};
    
    event.timestamp = bpf_ktime_get_ns();
    event.cpu = bpf_get_smp_processor_id();
    
    // Extract reason - cast to u32 to avoid enum issues
    event.reason = (u32)args->reason;
    
    event.stack_id = stack_traces_kfree.get_stackid(args, BPF_F_REUSE_STACKID);
    event.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    
    events_kfree.perf_submit(args, &event, sizeof(event));
    return 0;
}
"""

# BPF program for memory allocation failure
bpf_text_alloc = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>

struct alloc_fail_event {
    u64 timestamp;
    u32 cpu;
    u32 stack_id;
    u32 pid;
    char comm[TASK_COMM_LEN];
    u32 gfp_mask;
};

BPF_STACK_TRACE(stack_traces_alloc, 1024);
BPF_PERF_OUTPUT(events_alloc);

int trace_warn_alloc(struct pt_regs *ctx) {
    struct alloc_fail_event event = {};
    
    event.timestamp = bpf_ktime_get_ns();
    event.cpu = bpf_get_smp_processor_id();
    event.stack_id = stack_traces_alloc.get_stackid(ctx, BPF_F_REUSE_STACKID);
    event.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    event.gfp_mask = (u32)PT_REGS_PARM1(ctx);
    
    events_alloc.perf_submit(ctx, &event, sizeof(event));
    return 0;
}
"""

# Network RX path indicators
NET_RX_INDICATORS = [
    'netif_receive_skb',
    '__netif_receive_skb',
    '__netif_receive_skb_core',
    'net_rx_action',
    'napi_gro_receive',
    'virtnet_poll',
    'try_fill_recv',
    'igb_clean_rx_irq',
    'ixgbe_clean_rx_irq',
]

def is_net_rx_path(bpf_obj, stack_traces, stack_id):
    """Check if stack trace is in network RX path"""
    try:
        stack = stack_traces.walk(stack_id)
        for addr in stack:
            sym = bpf_obj.ksym(addr)
            if sym:
                for indicator in NET_RX_INDICATORS:
                    if indicator in sym:
                        return True
    except:
        pass
    return False

class DropDetector:
    """Module 1: Drop detection module"""
    def __init__(self, interfaces):
        self.interfaces = interfaces
        self.initial_stats = {}
        self.final_stats = {}
        self.drops_detected = False
        
    def start(self):
        """Start detection - read initial stats"""
        _, self.initial_stats = read_proc_net_dev()
        
    def stop(self):
        """Stop detection - read final stats and check for drops"""
        _, self.final_stats = read_proc_net_dev()
        self.drops_detected = False
        
        for ifname in self.interfaces:
            initial = self.initial_stats.get(ifname, 0)
            final = self.final_stats.get(ifname, 0)
            if final > initial:
                self.drops_detected = True
                break
                
        return self.drops_detected
    
    def get_drop_count(self, ifname):
        """Get drop count for interface"""
        initial = self.initial_stats.get(ifname, 0)
        final = self.final_stats.get(ifname, 0)
        return final - initial

class StackCollector:
    """Module 2: Drop stack trace collector"""
    def __init__(self, interfaces, bpf_obj):
        self.interfaces = interfaces
        self.bpf = bpf_obj
        self.stack_traces = bpf_obj.get_table("stack_traces_kfree")
        self.stacks = defaultdict(int)
        self.total_count = 0
        
    def process_event(self, cpu, data, size):
        """Process kfree_skb event"""
        event = self.bpf["events_kfree"].event(data)
        
        # Note: Kernel-side filter already removed most non-RX events (softirq check)
        # This is a secondary check for precise RX path detection
        if not is_net_rx_path(self.bpf, self.stack_traces, event.stack_id):
            return
        
        # Note: Interface filtering is not implemented to avoid complex kernel headers
        # that cause BPF compilation errors. The tool monitors all interfaces and
        # correlates drops with /proc/net/dev statistics for the specified interfaces.
            
        # Record stack trace
        stack_key = str(event.stack_id)
        self.stacks[stack_key] += 1
        self.total_count += 1
        
    def get_top_stacks(self, n=3):
        """Get top N stack traces"""
        sorted_stacks = sorted(self.stacks.items(), key=lambda x: x[1], reverse=True)
        return sorted_stacks[:n]
    
    def print_results(self):
        """Print results"""
        if self.total_count == 0:
            return
            
        print("\n=== Drop Stack Trace Module ===")
        print("Total drop events in RX context: %d" % self.total_count)
        print("\nTop 3 stack traces:")
        
        top_stacks = self.get_top_stacks(3)
        for idx, (stack_key, count) in enumerate(top_stacks, 1):
            print("\n%d. [%d times]" % (idx, count))
            try:
                stack_id = int(stack_key)
                stack = self.stack_traces.walk(stack_id)
                for addr in stack:
                    sym = self.bpf.ksym(addr)
                    if sym:
                        print("    %s" % sym)
            except:
                print("    [Unable to resolve stack]")

class MemoryDetector:
    """Module 3: Memory allocation failure detector"""
    def __init__(self, interfaces, bpf_obj):
        self.interfaces = interfaces
        self.bpf = bpf_obj
        self.stack_traces = bpf_obj.get_table("stack_traces_alloc")
        self.stacks = defaultdict(int)
        self.total_count = 0
        
    def process_event(self, cpu, data, size):
        """Process memory allocation failure event"""
        event = self.bpf["events_alloc"].event(data)
        
        # Check if in RX path
        if not is_net_rx_path(self.bpf, self.stack_traces, event.stack_id):
            return
            
        # Record stack trace
        stack_key = str(event.stack_id)
        self.stacks[stack_key] += 1
        self.total_count += 1
        
    def get_top_stacks(self, n=3):
        """Get top N stack traces"""
        sorted_stacks = sorted(self.stacks.items(), key=lambda x: x[1], reverse=True)
        return sorted_stacks[:n]
    
    def print_results(self):
        """Print results"""
        if self.total_count == 0:
            return
            
        print("\n=== Memory Allocation Failure Module ===")
        print("Total alloc failures in RX context: %d" % self.total_count)
        print("\nTop 3 stack traces:")
        
        top_stacks = self.get_top_stacks(3)
        for idx, (stack_key, count) in enumerate(top_stacks, 1):
            print("\n%d. [%d times]" % (idx, count))
            try:
                stack_id = int(stack_key)
                stack = self.stack_traces.walk(stack_id)
                for addr in stack:
                    sym = self.bpf.ksym(addr)
                    if sym:
                        print("    %s" % sym)
            except:
                print("    [Unable to resolve stack]")

class SoftirqMonitor:
    """Module 5: CPU Softirq Utilization Monitor"""
    def __init__(self):
        self.initial_stats = {}  # {cpu_id: {'softirq': value, 'total': value}}
        self.final_stats = {}     # {cpu_id: {'softirq': value, 'total': value}}
        self.monitoring_duration = 0.0
        
    def _read_proc_stat(self):
        """Read /proc/stat and return CPU stats
        
        Returns:
            dict: {cpu_id: {'softirq': value, 'total': value}}
        """
        stats = {}
        try:
            with open('/proc/stat', 'r') as f:
                for line in f:
                    line = line.strip()
                    if line.startswith('cpu') and not line.startswith('cpu '):
                        # Parse cpu0, cpu1, etc.
                        parts = line.split()
                        if len(parts) >= 8:
                            cpu_id = parts[0]  # e.g., "cpu0"
                            cpu_num = cpu_id[3:]  # Extract number part
                            
                            # Parse CPU stats
                            # Format: cpu user nice system idle iowait irq softirq steal guest guest_nice
                            try:
                                user = int(parts[1])
                                nice = int(parts[2])
                                system = int(parts[3])
                                idle = int(parts[4])
                                iowait = int(parts[5]) if len(parts) > 5 else 0
                                irq = int(parts[6]) if len(parts) > 6 else 0
                                softirq = int(parts[7]) if len(parts) > 7 else 0
                                steal = int(parts[8]) if len(parts) > 8 else 0
                                guest = int(parts[9]) if len(parts) > 9 else 0
                                guest_nice = int(parts[10]) if len(parts) > 10 else 0
                                
                                # Total CPU time (all fields sum)
                                total = user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice
                                
                                stats[cpu_num] = {
                                    'softirq': softirq,
                                    'total': total
                                }
                            except (ValueError, IndexError):
                                continue
        except Exception as e:
            print("Warning: Failed to read /proc/stat: %s" % str(e))
        return stats
        
    def start(self):
        """Start monitoring - read initial stats"""
        self.initial_stats = self._read_proc_stat()
        
    def stop(self, duration):
        """Stop monitoring - read final stats
        
        Args:
            duration: Monitoring duration in seconds
        """
        if hasattr(self, '_stopped'):
            return
        self.monitoring_duration = duration
        self.final_stats = self._read_proc_stat()
        self._stopped = False
        
    def get_utilization(self, cpu_id):
        """Get softirq utilization for a specific CPU
        
        Args:
            cpu_id: CPU ID (string, e.g., "0", "1")
            
        Returns:
            tuple: (softirq_diff, total_diff, utilization_percent)
        """
        initial = self.initial_stats.get(cpu_id, {'softirq': 0, 'total': 0})
        final = self.final_stats.get(cpu_id, {'softirq': 0, 'total': 0})
        
        softirq_diff = final['softirq'] - initial['softirq']
        total_diff = final['total'] - initial['total']
        
        if total_diff > 0:
            # Utilization = (softirq_time / total_time) * 100
            # Note: /proc/stat values are in USER_HZ units (typically 100 Hz)
            # So we need to account for the actual time duration
            # total_diff is in USER_HZ ticks, duration is in seconds
            # USER_HZ is typically 100, so 1 tick = 10ms
            # But we can calculate utilization directly: softirq_diff / total_diff
            utilization = (softirq_diff / total_diff) * 100.0
        else:
            utilization = 0.0
            
        return softirq_diff, total_diff, utilization
        
    def print_results(self):
        """Print softirq utilization results - only called when drops are detected"""
        print("\n=== CPU Softirq Utilization Module ===")
        
        if not self.initial_stats or not self.final_stats:
            print("Warning: No CPU statistics available")
            return
            
        print("Monitoring duration: %.2f seconds" % self.monitoring_duration)
        print("\nCPU  SI%")
        print("-" * 25)
        
        # Get all CPU IDs and calculate utilization for each
        all_cpus = set(self.initial_stats.keys()) | set(self.final_stats.keys())
        cpu_utilizations = []
        
        for cpu_id in sorted(all_cpus, key=lambda x: int(x) if x.isdigit() else 999):
            _, _, utilization = self.get_utilization(cpu_id)
            cpu_utilizations.append((cpu_id, utilization))
        
        # Sort by utilization (descending) and take top 10
        cpu_utilizations.sort(key=lambda x: x[1], reverse=True)
        top_10_cpus = cpu_utilizations[:10]
        
        # Always print results, even if all utilizations are 0%
        if top_10_cpus:
            for cpu_id, utilization in top_10_cpus:
                print("CPU%-3s %.2f%%" % (cpu_id, utilization))
        else:
            print("No CPU data available")

class NICQueueChecker:
    """Module 4: NIC queue checker"""
    def __init__(self, interfaces):
        self.interfaces = interfaces
        self.ethtool_g_info = {}
        self.initial_stats = {}
        self.final_stats = {}
        self._ethtool_cmd = None  # Cache ethtool command path
        
        # Key variables to extract from ethtool -S
        self.key_variables = [
            'rx_dropped',
            'rx_errors',
            'rx_missed_errors',
            'rx_no_buffer',
            'rx_fifo_errors',
            'rx_over_errors',
            'rx_out_of_buffer',
        ]
    
    def _find_ethtool_cmd(self):
        """Find ethtool command path and cache it"""
        if self._ethtool_cmd is not None:
            return self._ethtool_cmd
        
        ethtool_cmd = 'ethtool'
        try:
            result = subprocess.run(['which', 'ethtool'], 
                                  stdout=subprocess.PIPE, stderr=subprocess.PIPE, 
                                  universal_newlines=True, timeout=2)
            if result.returncode == 0 and result.stdout.strip():
                ethtool_cmd = result.stdout.strip()
        except:
            pass  # Use default 'ethtool'
        
        self._ethtool_cmd = ethtool_cmd
        return ethtool_cmd
        
    def _get_ethtool_stats(self, ifname):
        """Get ethtool statistics for an interface"""
        ethtool_cmd = self._find_ethtool_cmd()
        
        stats = {}
        try:
            result = subprocess.run([ethtool_cmd, '-S', ifname],
                                  stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                  universal_newlines=True, timeout=5)
            if result.returncode == 0:
                for line in result.stdout.split('\n'):
                    line = line.strip()
                    if ':' in line:
                        parts = line.split(':', 1)
                        if len(parts) == 2:
                            key = parts[0].strip()
                            value = parts[1].strip()
                            # Check if this is one of our key variables
                            for var in self.key_variables:
                                if var.lower() in key.lower():
                                    try:
                                        stats[key] = int(value)
                                    except ValueError:
                                        stats[key] = value
                                    break
        except:
            pass
        return stats
    
    def start(self):
        """Start detection - read initial stats"""
        for ifname in self.interfaces:
            self.initial_stats[ifname] = self._get_ethtool_stats(ifname)
    
    def stop(self):
        """Stop detection - read final stats"""
        for ifname in self.interfaces:
            self.final_stats[ifname] = self._get_ethtool_stats(ifname)
        self._stopped = True
    
    def collect_info(self):
        """Collect ethtool ring buffer information (ethtool -g)"""
        ethtool_cmd = self._find_ethtool_cmd()
        
        for ifname in self.interfaces:
            # ethtool -g: Get ring buffer info
            try:
                result = subprocess.run([ethtool_cmd, '-g', ifname],
                                      stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                      universal_newlines=True, timeout=5)
                if result.returncode == 0:
                    self.ethtool_g_info[ifname] = result.stdout
                else:
                    error_msg = result.stderr.strip() if result.stderr else "Return code: %d" % result.returncode
                    self.ethtool_g_info[ifname] = "Failed: %s" % error_msg
            except subprocess.TimeoutExpired:
                self.ethtool_g_info[ifname] = "Failed: Command timeout"
            except FileNotFoundError:
                self.ethtool_g_info[ifname] = "Failed: ethtool command not found (tried: %s)" % ethtool_cmd
            except Exception as e:
                self.ethtool_g_info[ifname] = "Failed: %s" % str(e)
    
    def print_results(self):
        """Print results"""
        print("\n=== NIC Queue Check Module ===")
        for ifname in self.interfaces:
            print("\nInterface: %s" % ifname)
            print("\nRing buffer info (ethtool -g):")
            print(self.ethtool_g_info.get(ifname, "N/A"))
            
            print("\nKey statistics (ethtool -S):")
            initial = self.initial_stats.get(ifname, {})
            final = self.final_stats.get(ifname, {})
            
            # Get all keys from both initial and final stats
            all_keys = set(initial.keys()) | set(final.keys())
            
            if all_keys:
                for key in sorted(all_keys):
                    initial_val = initial.get(key, 0)
                    final_val = final.get(key, 0)
                    
                    # Handle both integer and string values
                    if isinstance(initial_val, int) and isinstance(final_val, int):
                        diff = final_val - initial_val
                        print("  %s:" % key)
                        print("    Previous value: %d" % initial_val)
                        print("    Current value:  %d" % final_val)
                        print("    Difference:     %d" % diff)
                    else:
                        # For non-integer values, just show current value
                        print("  %s: %s" % (key, final_val if final_val else initial_val))
            else:
                print("  No key statistics found")

def run_iteration(iteration_num, duration, interfaces):
    """Run one iteration
    
    Returns:
        bool: True if drops were detected, False otherwise
    """
    print("\n" + "="*60)
    print("Iteration %d/%d (Duration: %d seconds)" % (iteration_num, args.count, duration))
    print("="*60)
    
    # Initialize modules
    drop_detector = DropDetector(interfaces)
    stack_collector = None
    memory_detector = None
    nic_checker = NICQueueChecker(interfaces)
    softirq_monitor = SoftirqMonitor()
    bpf_kfree = None
    bpf_alloc = None
    
    # Load BPF programs (Module 2 and 3 start first)
    try:
        bpf_kfree = BPF(text=bpf_text_kfree_skb)
        bpf_alloc = BPF(text=bpf_text_alloc)
        
        # TRACEPOINT_PROBE(skb, kfree_skb) automatically attaches when BPF program loads
        print("kfree_skb tracepoint attached successfully")
        
        # Attach kprobe for warn_alloc
        try:
            bpf_alloc.attach_kprobe(event="warn_alloc", fn_name="trace_warn_alloc")
            print("warn_alloc kprobe attached successfully")
        except Exception as e:
            print("Warning: Failed to attach kprobe for warn_alloc: %s" % str(e))
            print("Memory allocation failure detection may not work")
        
        stack_collector = StackCollector(interfaces, bpf_kfree)
        memory_detector = MemoryDetector(interfaces, bpf_alloc)
        
        # Open perf buffers
        bpf_kfree["events_kfree"].open_perf_buffer(stack_collector.process_event)
        bpf_alloc["events_alloc"].open_perf_buffer(memory_detector.process_event)
        
        print("BPF programs loaded successfully")
    except Exception as e:
        print("Warning: Failed to load BPF programs: %s" % str(e))
        print("Continuing with drop detection only...")
    
    # Start monitoring
    start_time = time.time()
    end_time = start_time + duration
    
    print("\nMonitoring interfaces: %s" % ', '.join(interfaces))
    print("Monitoring started at: %s" % datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    
    # Start drop detection module last (Module 1: last to start)
    # Wait a bit to let other modules start first
    time.sleep(0.5)
    drop_detector.start()
    nic_checker.start()
    softirq_monitor.start()
    
    # Poll BPF events and monitor
    drop_detection_end_time = end_time - 1.0  # Stop drop detection 1 second earlier
    
    try:
        while time.time() < end_time:
            current_time = time.time()
            
            # Poll BPF events (Module 2 and 3: concurrent via async events)
            if bpf_kfree:
                bpf_kfree.perf_buffer_poll(timeout=100)
            if bpf_alloc:
                bpf_alloc.perf_buffer_poll(timeout=100)
            
            # Stop drop detection first (Module 1: first to stop)
            # Note: /proc/net/dev rx_dropped is cumulative, so we only need to read at start and end
            if current_time >= drop_detection_end_time and not hasattr(drop_detector, '_stopped'):
                drops_detected = drop_detector.stop()
                nic_checker.stop()
                actual_duration = current_time - start_time
                softirq_monitor.stop(actual_duration)
                drop_detector._stopped = True
                if drops_detected:
                    print("Drops detected! Other modules continue to capture data...")
                    # Let other modules continue for a bit more to ensure all drops are captured
                    time.sleep(0.5)
            
            time.sleep(0.1)
    except KeyboardInterrupt:
        print("\nInterrupted by user")
        if not hasattr(drop_detector, '_stopped'):
            actual_duration = time.time() - start_time
            drop_detector.stop()
            nic_checker.stop()
            softirq_monitor.stop(actual_duration)
    
    # Ensure all modules are stopped
    actual_duration = time.time() - start_time
    if not hasattr(drop_detector, '_stopped'):
        drops_detected = drop_detector.stop()
    else:
        drops_detected = drop_detector.drops_detected
    
    if not hasattr(nic_checker, '_stopped'):
        nic_checker.stop()
    
    if not hasattr(softirq_monitor, '_stopped'):
        softirq_monitor.stop(actual_duration)
    
    print("Monitoring ended at: %s" % datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    
    # Print drop statistics
    print("\n=== Drop Detection Module ===")
    for ifname in interfaces:
        initial = drop_detector.initial_stats.get(ifname, 0)
        final = drop_detector.final_stats.get(ifname, 0)
        drop_count = final - initial
        print("Interface %s:" % ifname)
        print("  Previous rx drops: %d" % initial)
        print("  Current rx drops:  %d" % final)
        print("  Difference:     %d" % drop_count)
    
    # Print other modules if drops were detected
    if drops_detected:
        if stack_collector:
            stack_collector.print_results()
        if memory_detector:
            memory_detector.print_results()
        
        # Module 4: Print ethtool info only if drops were detected
        # Ensure nic_checker.stop() has been called
        if not hasattr(nic_checker, '_stopped'):
            nic_checker.stop()
        nic_checker.collect_info()
        nic_checker.print_results()
        
        # Module 5: Print softirq utilization only if drops were detected
    softirq_monitor.print_results()
    
    # Cleanup BPF resources to prevent file descriptor leaks
    if bpf_kfree:
        # Tracepoint will be automatically detached during cleanup
        try:
            # Close perf buffer before cleanup
            bpf_kfree["events_kfree"].close_perf_buffer()
        except:
            pass
        try:
            bpf_kfree.cleanup()
        except:
            pass
        # Force deletion to ensure garbage collection
        del bpf_kfree
    if bpf_alloc:
        try:
            # Detach kprobe before cleanup
            bpf_alloc.detach_kprobe(event="warn_alloc")
        except:
            pass
        try:
            # Close perf buffer before cleanup
            bpf_alloc["events_alloc"].close_perf_buffer()
        except:
            pass
        try:
            bpf_alloc.cleanup()
        except:
            pass
        # Force deletion to ensure garbage collection
        del bpf_alloc
    
    # Force garbage collection to release file descriptors
    gc.collect()
    # Small sleep to allow system to reclaim resources
    time.sleep(0.1)
    
    # Return True if drops were detected
    return drops_detected

# Main execution
# If --ebpf is specified, print eBPF C code and exit
if args.ebpf:
    print("=== eBPF Program: kfree_skb stack trace ===")
    print(bpf_text_kfree_skb)
    print("\n" + "="*60 + "\n")
    print("=== eBPF Program: memory allocation failure ===")
    print(bpf_text_alloc)
    exit()

print("=== Network RX Drop Analysis Tool ===")
print("Interfaces: %s" % ', '.join(interfaces))
print("Iterations: %d" % args.count)
print("Duration per iteration: %d seconds" % args.duration)

for i in range(1, args.count + 1):
    drops_detected = run_iteration(i, args.duration, interfaces)
    if drops_detected:
        print("\n" + "="*60)
        print("Drops detected in iteration %d. Stopping monitoring." % i)
        print("="*60)
        break
    if i < args.count:
        time.sleep(1)  # Small delay between iterations
if not drops_detected:
    print("\n" + "="*60)
    print("No rx drops found for nic(s) %s after %d iterations." % (', '.join(interfaces), i))
    print("="*60)
