public inbox for kdevops@lists.linux.dev
 help / color / mirror / Atom feed
From: Luis Chamberlain <mcgrof@kernel.org>
To: Chuck Lever <cel@kernel.org>, Daniel Gomez <da.gomez@kruces.com>,
	kdevops@lists.linux.dev
Cc: Luis Chamberlain <mcgrof@kernel.org>
Subject: [PATCH 1/5] monitoring: add memory fragmentation eBPF monitoring support
Date: Thu,  4 Sep 2025 02:13:17 -0700	[thread overview]
Message-ID: <20250904091322.2499058-2-mcgrof@kernel.org> (raw)
In-Reply-To: <20250904091322.2499058-1-mcgrof@kernel.org>

Add support for memory fragmentation monitoring using eBPF-based tracking,
we leverage the plot-fragmentation effort [0]. This provides real-time
tracking of memory allocation events and fragmentation indices with matplotlib
visualization.

Features:
- eBPF tracepoint-based fragmentation tracking
- Real-time fragmentation index monitoring
- Automatic plot generation with fragmentation_visualizer.py
- Configurable monitoring duration and output directory
- Integration with existing monitoring framework

The scripts are included directly in kdevops rather than cloning
an external repository for simplicity.

Generated-by: Claude AI
Link: https://github.com/mcgrof/plot-fragmentation # [0]
Signed-off-by: Luis Chamberlain <mcgrof@kernel.org>
---
 kconfigs/monitors/Kconfig                     |   53 +
 .../tasks/install-deps/debian/main.yml        |    1 +
 .../tasks/install-deps/redhat/main.yml        |    1 +
 .../fstests/tasks/install-deps/suse/main.yml  |    1 +
 .../monitoring/files/fragmentation_tracker.py |  533 ++++++++
 .../files/fragmentation_visualizer.py         | 1161 +++++++++++++++++
 .../monitoring/tasks/monitor_collect.yml      |  126 ++
 .../roles/monitoring/tasks/monitor_run.yml    |  123 ++
 8 files changed, 1999 insertions(+)
 create mode 100644 playbooks/roles/monitoring/files/fragmentation_tracker.py
 create mode 100644 playbooks/roles/monitoring/files/fragmentation_visualizer.py

diff --git a/kconfigs/monitors/Kconfig b/kconfigs/monitors/Kconfig
index 6dc1ddbdd2e9..bd4dc81fa11d 100644
--- a/kconfigs/monitors/Kconfig
+++ b/kconfigs/monitors/Kconfig
@@ -61,6 +61,59 @@ config MONITOR_FOLIO_MIGRATION_INTERVAL
 	  performance. Higher values reduce overhead but may miss
 	  short-lived migration events.
 
+config MONITOR_MEMORY_FRAGMENTATION
+	bool "Monitor memory fragmentation with eBPF"
+	output yaml
+	default n
+	help
+	  Enable monitoring of memory fragmentation using eBPF-based tracking.
+	  This provides advanced memory fragmentation visualization using
+	  eBPF tracepoints and matplotlib.
+
+	  This tool tracks memory allocation events and fragmentation indices
+	  in real-time, providing insights that traditional methods like
+	  /proc/pagetypeinfo cannot fully capture.
+
+	  Features:
+	  - eBPF-based tracepoint tracking
+	  - Real-time fragmentation index monitoring
+	  - Page mobility tracking
+	  - Matplotlib visualization of fragmentation data
+
+	  Requirements:
+	  - Python 3 with python3-bpfcc
+	  - Kernel with required tracepoint support
+	  - Root privileges for eBPF attachment
+
+	  The tool is particularly useful for investigating whether Large Block
+	  Size support in the kernel creates worse fragmentation.
+
+config MONITOR_FRAGMENTATION_DURATION
+	int "Fragmentation monitoring duration (seconds)"
+	output yaml
+	default 0
+	depends on MONITOR_MEMORY_FRAGMENTATION
+	help
+	  Duration to run fragmentation monitoring in seconds.
+	  Set to 0 for continuous monitoring until workflow completion.
+
+	  The monitoring will automatically stop when the workflow
+	  finishes or when this duration expires, whichever comes first.
+
+config MONITOR_FRAGMENTATION_OUTPUT_DIR
+	string "Fragmentation monitoring output directory"
+	output yaml
+	default "/root/monitoring/fragmentation"
+	depends on MONITOR_MEMORY_FRAGMENTATION
+	help
+	  Directory where fragmentation monitoring data and plots will be stored.
+	  This directory will be created if it doesn't exist.
+
+	  The collected data includes:
+	  - Raw eBPF trace data
+	  - Generated matplotlib plots
+	  - JSON formatted fragmentation metrics
+
 endif # MONITOR_DEVELOPMENTAL_STATS
 
 # Future monitoring options can be added here
diff --git a/playbooks/roles/fstests/tasks/install-deps/debian/main.yml b/playbooks/roles/fstests/tasks/install-deps/debian/main.yml
index cbcb3788d2bd..cc4a5a6b10af 100644
--- a/playbooks/roles/fstests/tasks/install-deps/debian/main.yml
+++ b/playbooks/roles/fstests/tasks/install-deps/debian/main.yml
@@ -73,6 +73,7 @@
       - xfsdump
       - cifs-utils
       - duperemove
+      - python3-bpfcc
     state: present
     update_cache: true
   tags: ["fstests", "deps"]
diff --git a/playbooks/roles/fstests/tasks/install-deps/redhat/main.yml b/playbooks/roles/fstests/tasks/install-deps/redhat/main.yml
index c1bd7f82f0aa..3c681c1a06fb 100644
--- a/playbooks/roles/fstests/tasks/install-deps/redhat/main.yml
+++ b/playbooks/roles/fstests/tasks/install-deps/redhat/main.yml
@@ -72,6 +72,7 @@
       - gettext
       - ncurses
       - ncurses-devel
+      - python3-bcc
 
 - name: Install xfsprogs-xfs_scrub
   become: true
diff --git a/playbooks/roles/fstests/tasks/install-deps/suse/main.yml b/playbooks/roles/fstests/tasks/install-deps/suse/main.yml
index 3247567a8cfa..54de4a7ad3d5 100644
--- a/playbooks/roles/fstests/tasks/install-deps/suse/main.yml
+++ b/playbooks/roles/fstests/tasks/install-deps/suse/main.yml
@@ -124,6 +124,7 @@
       - libcap-progs
       - fio
       - parted
+      - python3-bcc
     state: present
   when:
     - repos_present|bool
diff --git a/playbooks/roles/monitoring/files/fragmentation_tracker.py b/playbooks/roles/monitoring/files/fragmentation_tracker.py
new file mode 100644
index 000000000000..7b66f3232960
--- /dev/null
+++ b/playbooks/roles/monitoring/files/fragmentation_tracker.py
@@ -0,0 +1,533 @@
+#!/usr/bin/env python3
+"""
+Enhanced eBPF-based memory fragmentation tracker.
+Primary focus on mm_page_alloc_extfrag events with optional compaction tracking.
+"""
+
+from bcc import BPF
+import time
+import signal
+import sys
+import os
+import json
+import argparse
+from collections import defaultdict
+from datetime import datetime
+
+# eBPF program to trace fragmentation events
+bpf_program = """
+#include <uapi/linux/ptrace.h>
+#include <linux/mm.h>
+#include <linux/mmzone.h>
+
+// Event types
+#define EVENT_COMPACTION_SUCCESS 1
+#define EVENT_COMPACTION_FAILURE 2
+#define EVENT_EXTFRAG 3
+
+struct fragmentation_event {
+    u64 timestamp;
+    u32 pid;
+    u32 tid;
+    u8 event_type;  // 1=compact_success, 2=compact_fail, 3=extfrag
+
+    // Common fields
+    u32 order;
+    int fragmentation_index;
+    int zone_idx;
+    int node_id;
+
+    // ExtFrag specific fields
+    int fallback_order;         // Order of the fallback allocation
+    int migrate_from;           // Original migrate type
+    int migrate_to;             // Fallback migrate type
+    int fallback_blocks;        // Number of pageblocks involved
+    int is_steal;               // Whether this is a steal vs claim
+
+    // Process info
+    char comm[16];
+};
+
+BPF_PERF_OUTPUT(events);
+
+// Statistics tracking
+BPF_HASH(extfrag_stats, u32, u64);  // Key: order, Value: count
+BPF_HASH(compact_stats, u32, u64);  // Key: order|success<<16, Value: count
+
+// Helper to get current fragmentation state (simplified)
+static inline int get_fragmentation_estimate(int order) {
+    // This is a simplified estimate
+    // In real implementation, we'd need to walk buddy lists
+    // For now, return a placeholder that indicates we need fragmentation data
+    if (order <= 3) return 100;  // Low order usually OK
+    if (order <= 6) return 400;  // Medium order moderate frag
+    return 700;  // High order typically fragmented
+}
+
+// Trace external fragmentation events (page steal/claim from different migratetype)
+TRACEPOINT_PROBE(kmem, mm_page_alloc_extfrag) {
+    struct fragmentation_event event = {};
+
+    event.timestamp = bpf_ktime_get_ns();
+    event.pid = bpf_get_current_pid_tgid() >> 32;
+    event.tid = bpf_get_current_pid_tgid() & 0xFFFFFFFF;
+    event.event_type = EVENT_EXTFRAG;
+
+    // Extract tracepoint arguments
+    // Note: Field names may vary by kernel version
+    // Typical fields: alloc_order, fallback_order,
+    //                alloc_migratetype, fallback_migratetype, change_ownership
+
+    event.order = args->alloc_order;
+    event.fallback_order = args->fallback_order;
+    event.migrate_from = args->fallback_migratetype;
+    event.migrate_to = args->alloc_migratetype;
+
+    // change_ownership indicates if the whole pageblock was claimed
+    // 0 = steal (partial), 1 = claim (whole block)
+    event.is_steal = args->change_ownership ? 0 : 1;
+
+    // Node ID - set to -1 as page struct access is kernel-specific
+    // Could be enhanced with kernel version detection
+    event.node_id = -1;
+    event.zone_idx = -1;
+
+    // Estimate fragmentation at this point
+    event.fragmentation_index = get_fragmentation_estimate(event.order);
+
+    // Get process name
+    bpf_get_current_comm(&event.comm, sizeof(event.comm));
+
+    events.perf_submit(args, &event, sizeof(event));
+
+    // Update statistics
+    u64 *count = extfrag_stats.lookup(&event.order);
+    if (count) {
+        (*count)++;
+    } else {
+        u64 initial = 1;
+        extfrag_stats.update(&event.order, &initial);
+    }
+
+    return 0;
+}
+
+// Optional: Trace compaction success (if tracepoint exists)
+#ifdef TRACE_COMPACTION
+TRACEPOINT_PROBE(page_alloc, mm_compaction_success) {
+    struct fragmentation_event event = {};
+
+    event.timestamp = bpf_ktime_get_ns();
+    event.pid = bpf_get_current_pid_tgid() >> 32;
+    event.tid = bpf_get_current_pid_tgid() & 0xFFFFFFFF;
+    event.event_type = EVENT_COMPACTION_SUCCESS;
+
+    event.order = args->order;
+    event.fragmentation_index = args->ret;
+    event.zone_idx = args->idx;
+    event.node_id = args->nid;
+
+    bpf_get_current_comm(&event.comm, sizeof(event.comm));
+
+    events.perf_submit(args, &event, sizeof(event));
+
+    u32 key = (event.order) | (1 << 16);  // Set success bit
+    u64 *count = compact_stats.lookup(&key);
+    if (count) {
+        (*count)++;
+    } else {
+        u64 initial = 1;
+        compact_stats.update(&key, &initial);
+    }
+
+    return 0;
+}
+
+TRACEPOINT_PROBE(page_alloc, mm_compaction_failure) {
+    struct fragmentation_event event = {};
+
+    event.timestamp = bpf_ktime_get_ns();
+    event.pid = bpf_get_current_pid_tgid() >> 32;
+    event.tid = bpf_get_current_pid_tgid() & 0xFFFFFFFF;
+    event.event_type = EVENT_COMPACTION_FAILURE;
+
+    event.order = args->order;
+    event.fragmentation_index = -1;
+    event.zone_idx = -1;
+    event.node_id = -1;
+
+    bpf_get_current_comm(&event.comm, sizeof(event.comm));
+
+    events.perf_submit(args, &event, sizeof(event));
+
+    u32 key = event.order;  // No success bit
+    u64 *count = compact_stats.lookup(&key);
+    if (count) {
+        (*count)++;
+    } else {
+        u64 initial = 1;
+        compact_stats.update(&key, &initial);
+    }
+
+    return 0;
+}
+#endif
+"""
+
+# Migrate type names for better readability
+MIGRATE_TYPES = {
+    0: "UNMOVABLE",
+    1: "MOVABLE",
+    2: "RECLAIMABLE",
+    3: "PCPTYPES",
+    4: "HIGHATOMIC",
+    5: "CMA",
+    6: "ISOLATE",
+}
+
+
+class FragmentationTracker:
+    def __init__(self, verbose=True, output_file=None):
+        self.start_time = time.time()
+        self.events_data = []
+        self.extfrag_stats = defaultdict(int)
+        self.compact_stats = defaultdict(lambda: {"success": 0, "failure": 0})
+        self.zone_names = ["DMA", "DMA32", "Normal", "Movable", "Device"]
+        self.verbose = verbose
+        self.output_file = output_file
+        self.event_count = 0
+        self.interrupted = False
+
+    def process_event(self, cpu, data, size):
+        """Process a fragmentation event from eBPF."""
+        event = self.b["events"].event(data)
+
+        # Calculate relative time from start
+        rel_time = (event.timestamp - self.start_ns) / 1e9
+
+        # Decode process name
+        try:
+            comm = event.comm.decode("utf-8", "replace")
+        except:
+            comm = "unknown"
+
+        # Determine event type and format output
+        if event.event_type == 3:  # EXTFRAG event
+            event_name = "EXTFRAG"
+            color = "\033[93m"  # Yellow
+
+            # Get migrate type names
+            from_type = MIGRATE_TYPES.get(
+                event.migrate_from, f"TYPE_{event.migrate_from}"
+            )
+            to_type = MIGRATE_TYPES.get(event.migrate_to, f"TYPE_{event.migrate_to}")
+
+            # Store event data
+            event_dict = {
+                "timestamp": rel_time,
+                "absolute_time": datetime.now().isoformat(),
+                "event_type": "extfrag",
+                "pid": event.pid,
+                "tid": event.tid,
+                "comm": comm,
+                "order": event.order,
+                "fallback_order": event.fallback_order,
+                "migrate_from": from_type,
+                "migrate_to": to_type,
+                "is_steal": bool(event.is_steal),
+                "node": event.node_id,
+                "fragmentation_index": event.fragmentation_index,
+            }
+
+            self.extfrag_stats[event.order] += 1
+
+            if self.verbose:
+                action = "steal" if event.is_steal else "claim"
+                print(
+                    f"{color}[{rel_time:8.3f}s] {event_name:10s}\033[0m "
+                    f"Order={event.order:2d} FallbackOrder={event.fallback_order:2d} "
+                    f"{from_type:10s}->{to_type:10s} ({action}) "
+                    f"Process={comm:12s} PID={event.pid:6d}"
+                )
+
+        elif event.event_type == 1:  # COMPACTION_SUCCESS
+            event_name = "COMPACT_OK"
+            color = "\033[92m"  # Green
+
+            zone_name = (
+                self.zone_names[event.zone_idx]
+                if 0 <= event.zone_idx < len(self.zone_names)
+                else "Unknown"
+            )
+
+            event_dict = {
+                "timestamp": rel_time,
+                "absolute_time": datetime.now().isoformat(),
+                "event_type": "compaction_success",
+                "pid": event.pid,
+                "comm": comm,
+                "order": event.order,
+                "fragmentation_index": event.fragmentation_index,
+                "zone": zone_name,
+                "node": event.node_id,
+            }
+
+            self.compact_stats[event.order]["success"] += 1
+
+            if self.verbose:
+                print(
+                    f"{color}[{rel_time:8.3f}s] {event_name:10s}\033[0m "
+                    f"Order={event.order:2d} FragIdx={event.fragmentation_index:5d} "
+                    f"Zone={zone_name:8s} Node={event.node_id:2d} "
+                    f"Process={comm:12s} PID={event.pid:6d}"
+                )
+
+        else:  # COMPACTION_FAILURE
+            event_name = "COMPACT_FAIL"
+            color = "\033[91m"  # Red
+
+            event_dict = {
+                "timestamp": rel_time,
+                "absolute_time": datetime.now().isoformat(),
+                "event_type": "compaction_failure",
+                "pid": event.pid,
+                "comm": comm,
+                "order": event.order,
+                "fragmentation_index": -1,
+            }
+
+            self.compact_stats[event.order]["failure"] += 1
+
+            if self.verbose:
+                print(
+                    f"{color}[{rel_time:8.3f}s] {event_name:10s}\033[0m "
+                    f"Order={event.order:2d} "
+                    f"Process={comm:12s} PID={event.pid:6d}"
+                )
+
+        self.events_data.append(event_dict)
+        self.event_count += 1
+
+    def print_summary(self):
+        """Print summary statistics."""
+        print("\n" + "=" * 80)
+        print("FRAGMENTATION TRACKING SUMMARY")
+        print("=" * 80)
+
+        total_events = len(self.events_data)
+        print(f"\nTotal events captured: {total_events}")
+
+        if total_events > 0:
+            # Count by type
+            extfrag_count = sum(
+                1 for e in self.events_data if e["event_type"] == "extfrag"
+            )
+            compact_success = sum(
+                1 for e in self.events_data if e["event_type"] == "compaction_success"
+            )
+            compact_fail = sum(
+                1 for e in self.events_data if e["event_type"] == "compaction_failure"
+            )
+
+            print(f"\nEvent breakdown:")
+            print(f"  External Fragmentation: {extfrag_count}")
+            print(f"  Compaction Success: {compact_success}")
+            print(f"  Compaction Failure: {compact_fail}")
+
+            # ExtFrag analysis
+            if extfrag_count > 0:
+                print("\nExternal Fragmentation Events by Order:")
+                print("-" * 40)
+                print(f"{'Order':<8} {'Count':<10} {'Percentage':<10}")
+                print("-" * 40)
+
+                for order in sorted(self.extfrag_stats.keys()):
+                    count = self.extfrag_stats[order]
+                    pct = (count / extfrag_count) * 100
+                    print(f"{order:<8} {count:<10} {pct:<10.1f}%")
+
+                # Analyze migrate type patterns
+                extfrag_events = [
+                    e for e in self.events_data if e["event_type"] == "extfrag"
+                ]
+                migrate_patterns = defaultdict(int)
+                steal_vs_claim = {"steal": 0, "claim": 0}
+
+                for e in extfrag_events:
+                    pattern = f"{e['migrate_from']}->{e['migrate_to']}"
+                    migrate_patterns[pattern] += 1
+                    if e["is_steal"]:
+                        steal_vs_claim["steal"] += 1
+                    else:
+                        steal_vs_claim["claim"] += 1
+
+                print("\nMigrate Type Patterns:")
+                print("-" * 40)
+                for pattern, count in sorted(
+                    migrate_patterns.items(), key=lambda x: x[1], reverse=True
+                )[:5]:
+                    print(
+                        f"  {pattern:<30} {count:5d} ({count/extfrag_count*100:5.1f}%)"
+                    )
+
+                print(f"\nSteal vs Claim:")
+                print(
+                    f"  Steal (partial): {steal_vs_claim['steal']} ({steal_vs_claim['steal']/extfrag_count*100:.1f}%)"
+                )
+                print(
+                    f"  Claim (whole):   {steal_vs_claim['claim']} ({steal_vs_claim['claim']/extfrag_count*100:.1f}%)"
+                )
+
+            # Compaction analysis
+            if self.compact_stats:
+                print("\nCompaction Events by Order:")
+                print("-" * 40)
+                print(
+                    f"{'Order':<8} {'Success':<10} {'Failure':<10} {'Total':<10} {'Success%':<10}"
+                )
+                print("-" * 40)
+
+                for order in sorted(self.compact_stats.keys()):
+                    stats = self.compact_stats[order]
+                    total = stats["success"] + stats["failure"]
+                    success_pct = (stats["success"] / total * 100) if total > 0 else 0
+                    print(
+                        f"{order:<8} {stats['success']:<10} {stats['failure']:<10} "
+                        f"{total:<10} {success_pct:<10.1f}"
+                    )
+
+    def save_data(self, filename=None):
+        """Save captured data to JSON file for visualization."""
+        if filename is None and self.output_file:
+            filename = self.output_file
+
+        if filename is None:
+            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+            filename = f"fragmentation_data_{timestamp}.json"
+
+        # Prepare statistics
+        stats = {}
+
+        # ExtFrag stats
+        stats["extfrag"] = dict(self.extfrag_stats)
+
+        # Compaction stats
+        stats["compaction"] = {}
+        for order, counts in self.compact_stats.items():
+            stats["compaction"][str(order)] = counts
+
+        output = {
+            "metadata": {
+                "start_time": self.start_time,
+                "end_time": time.time(),
+                "duration": time.time() - self.start_time,
+                "total_events": len(self.events_data),
+                "kernel_version": os.uname().release,
+            },
+            "events": self.events_data,
+            "statistics": stats,
+        }
+
+        with open(filename, "w") as f:
+            json.dump(output, f, indent=2)
+        print(f"\nData saved to {filename}")
+        return filename
+
+    def run(self):
+        """Main execution loop."""
+        print("Compiling eBPF program...")
+
+        # Check if compaction tracepoints are available
+        has_compaction = os.path.exists(
+            "/sys/kernel/debug/tracing/events/page_alloc/mm_compaction_success"
+        )
+
+        # Modify BPF program based on available tracepoints
+        program = bpf_program
+        if has_compaction:
+            program = program.replace("#ifdef TRACE_COMPACTION", "#if 1")
+            print("  Compaction tracepoints: AVAILABLE")
+        else:
+            program = program.replace("#ifdef TRACE_COMPACTION", "#if 0")
+            print("  Compaction tracepoints: NOT AVAILABLE (will track extfrag only)")
+
+        self.b = BPF(text=program)
+        self.start_ns = time.perf_counter_ns()
+
+        # Setup event handler
+        self.b["events"].open_perf_buffer(self.process_event)
+
+        # Determine output filename upfront
+        if self.output_file:
+            save_file = self.output_file
+        else:
+            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+            save_file = f"fragmentation_data_{timestamp}.json"
+
+        print("\nStarting fragmentation event tracking...")
+        print(f"Primary focus: mm_page_alloc_extfrag events")
+        print(f"Data will be saved to: {save_file}")
+        print("Press Ctrl+C to stop and see summary\n")
+        print("-" * 80)
+        print(f"{'Time':>10s} {'Event':>12s} {'Details'}")
+        print("-" * 80)
+
+        try:
+            while not self.interrupted:
+                self.b.perf_buffer_poll()
+        except KeyboardInterrupt:
+            self.interrupted = True
+        finally:
+            # Always save data on exit
+            self.print_summary()
+            self.save_data()
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Track memory fragmentation events using eBPF"
+    )
+    parser.add_argument("-o", "--output", help="Output JSON file")
+    parser.add_argument(
+        "-t", "--time", type=int, help="Run for specified seconds then exit"
+    )
+    parser.add_argument(
+        "-q",
+        "--quiet",
+        action="store_true",
+        help="Suppress event output (summary only)",
+    )
+
+    args = parser.parse_args()
+
+    # Check for root privileges
+    if os.geteuid() != 0:
+        print("This script must be run as root (uses eBPF)")
+        sys.exit(1)
+
+    # Create tracker instance
+    tracker = FragmentationTracker(verbose=not args.quiet, output_file=args.output)
+
+    # Set up signal handler
+    def signal_handler_with_tracker(sig, frame):
+        tracker.interrupted = True
+
+    signal.signal(signal.SIGINT, signal_handler_with_tracker)
+
+    if args.time:
+        # Run for specified time
+        import threading
+
+        def timeout_handler():
+            time.sleep(args.time)
+            tracker.interrupted = True
+
+        timer = threading.Thread(target=timeout_handler)
+        timer.daemon = True
+        timer.start()
+
+    tracker.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/playbooks/roles/monitoring/files/fragmentation_visualizer.py b/playbooks/roles/monitoring/files/fragmentation_visualizer.py
new file mode 100644
index 000000000000..f3891de61e35
--- /dev/null
+++ b/playbooks/roles/monitoring/files/fragmentation_visualizer.py
@@ -0,0 +1,1161 @@
+#!/usr/bin/env python3
+"""
+Enhanced fragmentation A/B comparison with overlaid visualizations.
+Combines datasets on the same graphs using different visual markers.
+
+Usage:
+  python c6.py fragmentation_data_A.json --compare fragmentation_data_B.json -o comparison.png
+"""
+import json
+import sys
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib.gridspec as gridspec
+import matplotlib.patches as mpatches
+from matplotlib.patches import Rectangle
+from datetime import datetime
+import argparse
+from collections import defaultdict
+
+
+def load_data(filename):
+    with open(filename, "r") as f:
+        return json.load(f)
+
+
+def get_dot_size(order: int) -> float:
+    base_size = 1
+    return base_size + (order * 2)
+
+
+def build_counts(events, bin_size):
+    if not events:
+        return np.array([]), np.array([])
+    times = np.array([e["timestamp"] for e in events], dtype=float)
+    tmin, tmax = times.min(), times.max()
+    if tmax == tmin:
+        tmax = tmin + bin_size
+    bins = np.arange(tmin, tmax + bin_size, bin_size)
+    counts, edges = np.histogram(times, bins=bins)
+    centers = (edges[:-1] + edges[1:]) / 2.0
+    return centers, counts
+
+
+def get_migrate_type_color(mtype):
+    """Get consistent colors for migrate types"""
+    colors = {
+        "UNMOVABLE": "#e74c3c",  # Red
+        "MOVABLE": "#2ecc71",  # Green
+        "RECLAIMABLE": "#f39c12",  # Orange
+        "PCPTYPES": "#9b59b6",  # Purple
+        "HIGHATOMIC": "#e91e63",  # Pink
+        "ISOLATE": "#607d8b",  # Blue-grey
+        "CMA": "#00bcd4",  # Cyan
+    }
+    return colors.get(mtype, "#95a5a6")
+
+
+def get_migration_severity(from_type, to_type):
+    """Determine migration severity score"""
+    if to_type == "UNMOVABLE":
+        return -2  # Very bad
+    elif from_type == "UNMOVABLE":
+        return 1  # Good
+    elif to_type == "MOVABLE":
+        return 1  # Good
+    elif from_type == "MOVABLE" and to_type == "RECLAIMABLE":
+        return -1  # Somewhat bad
+    elif to_type == "RECLAIMABLE":
+        return -0.5  # Slightly bad
+    return 0
+
+
+def get_severity_color(severity_score):
+    """Get color based on severity score"""
+    if severity_score <= -2:
+        return "#8b0000"  # Dark red
+    elif severity_score <= -1:
+        return "#ff6b6b"  # Light red
+    elif severity_score >= 1:
+        return "#51cf66"  # Green
+    else:
+        return "#ffd43b"  # Yellow
+
+
+def create_overlaid_compaction_graph(ax, data_a, data_b, labels):
+    """Create overlaid compaction events graph"""
+
+    # Process dataset A
+    events_a = data_a.get("events", [])
+    compact_a = [
+        e
+        for e in events_a
+        if e["event_type"] in ["compaction_success", "compaction_failure"]
+    ]
+    success_a = [e for e in compact_a if e["event_type"] == "compaction_success"]
+    failure_a = [e for e in compact_a if e["event_type"] == "compaction_failure"]
+
+    # Process dataset B
+    events_b = data_b.get("events", [])
+    compact_b = [
+        e
+        for e in events_b
+        if e["event_type"] in ["compaction_success", "compaction_failure"]
+    ]
+    success_b = [e for e in compact_b if e["event_type"] == "compaction_success"]
+    failure_b = [e for e in compact_b if e["event_type"] == "compaction_failure"]
+
+    # Plot A with circles
+    for e in success_a:
+        ax.scatter(
+            e["timestamp"],
+            e.get("fragmentation_index", 0),
+            s=get_dot_size(e["order"]),
+            c="#2ecc71",
+            alpha=0.3,
+            edgecolors="none",
+            marker="o",
+            label=None,
+        )
+
+    for i, e in enumerate(failure_a):
+        y_pos = -50 - (e["order"] * 10)
+        ax.scatter(
+            e["timestamp"],
+            y_pos,
+            s=get_dot_size(e["order"]),
+            c="#e74c3c",
+            alpha=0.3,
+            edgecolors="none",
+            marker="o",
+            label=None,
+        )
+
+    # Plot B with triangles
+    for e in success_b:
+        ax.scatter(
+            e["timestamp"],
+            e.get("fragmentation_index", 0),
+            s=get_dot_size(e["order"]) * 1.2,
+            c="#27ae60",
+            alpha=0.4,
+            edgecolors="black",
+            linewidths=0.5,
+            marker="^",
+            label=None,
+        )
+
+    for i, e in enumerate(failure_b):
+        y_pos = -55 - (e["order"] * 10)  # Slightly offset from A
+        ax.scatter(
+            e["timestamp"],
+            y_pos,
+            s=get_dot_size(e["order"]) * 1.2,
+            c="#c0392b",
+            alpha=0.4,
+            edgecolors="black",
+            linewidths=0.5,
+            marker="^",
+            label=None,
+        )
+
+    # Set y-axis limits - cap at 1000, ignore data above
+    all_y_values = []
+    if success_a:
+        all_y_values.extend(
+            [
+                e.get("fragmentation_index", 0)
+                for e in success_a
+                if e.get("fragmentation_index", 0) <= 1000
+            ]
+        )
+    if success_b:
+        all_y_values.extend(
+            [
+                e.get("fragmentation_index", 0)
+                for e in success_b
+                if e.get("fragmentation_index", 0) <= 1000
+            ]
+        )
+
+    max_y = max(all_y_values) if all_y_values else 1000
+    min_y = -200  # Fixed minimum for failure lanes
+    ax.set_ylim(min_y, min(max_y + 100, 1000))
+
+    # Create legend - position above the data
+    from matplotlib.lines import Line2D
+
+    legend_elements = [
+        Line2D(
+            [0],
+            [0],
+            marker="o",
+            color="w",
+            markerfacecolor="#2ecc71",
+            markersize=8,
+            alpha=0.6,
+            label=f"{labels[0]} Success",
+        ),
+        Line2D(
+            [0],
+            [0],
+            marker="o",
+            color="w",
+            markerfacecolor="#e74c3c",
+            markersize=8,
+            alpha=0.6,
+            label=f"{labels[0]} Failure",
+        ),
+        Line2D(
+            [0],
+            [0],
+            marker="^",
+            color="w",
+            markerfacecolor="#27ae60",
+            markersize=8,
+            alpha=0.6,
+            label=f"{labels[1]} Success",
+        ),
+        Line2D(
+            [0],
+            [0],
+            marker="^",
+            color="w",
+            markerfacecolor="#c0392b",
+            markersize=8,
+            alpha=0.6,
+            label=f"{labels[1]} Failure",
+        ),
+    ]
+    # Position legend at y=0 on the left side where there's no data
+    ax.legend(
+        handles=legend_elements,
+        loc="center left",
+        bbox_to_anchor=(0.02, 0.5),
+        ncol=1,
+        fontsize=8,
+        frameon=True,
+        fancybox=True,
+    )
+
+    # Styling
+    ax.axhline(y=0, color="#34495e", linestyle="-", linewidth=1.5, alpha=0.8)
+    ax.grid(True, alpha=0.08, linestyle=":", linewidth=0.5)
+    ax.set_xlabel("Time (seconds)", fontsize=11)
+    ax.set_ylabel("Fragmentation Index", fontsize=11)
+    ax.set_title(
+        "Compaction Events Comparison (○ = A, △ = B)",
+        fontsize=13,
+        fontweight="bold",
+        pad=20,
+    )
+
+
+def create_overlaid_extfrag_timeline(ax, data_a, data_b, labels, bin_size=0.5):
+    """Create overlaid ExtFrag timeline"""
+
+    events_a = [e for e in data_a.get("events", []) if e["event_type"] == "extfrag"]
+    events_b = [e for e in data_b.get("events", []) if e["event_type"] == "extfrag"]
+
+    # Dataset A - solid lines
+    steal_a = [e for e in events_a if e.get("is_steal")]
+    claim_a = [e for e in events_a if not e.get("is_steal")]
+
+    steal_times_a, steal_counts_a = build_counts(steal_a, bin_size)
+    claim_times_a, claim_counts_a = build_counts(claim_a, bin_size)
+
+    if steal_times_a.size > 0:
+        ax.plot(
+            steal_times_a,
+            steal_counts_a,
+            linewidth=2,
+            color="#3498db",
+            alpha=0.8,
+            label=f"{labels[0]} Steal",
+            linestyle="-",
+        )
+        ax.fill_between(steal_times_a, 0, steal_counts_a, alpha=0.15, color="#3498db")
+
+    if claim_times_a.size > 0:
+        ax.plot(
+            claim_times_a,
+            claim_counts_a,
+            linewidth=2,
+            color="#e67e22",
+            alpha=0.8,
+            label=f"{labels[0]} Claim",
+            linestyle="-",
+        )
+        ax.fill_between(claim_times_a, 0, claim_counts_a, alpha=0.15, color="#e67e22")
+
+    # Dataset B - dashed lines
+    steal_b = [e for e in events_b if e.get("is_steal")]
+    claim_b = [e for e in events_b if not e.get("is_steal")]
+
+    steal_times_b, steal_counts_b = build_counts(steal_b, bin_size)
+    claim_times_b, claim_counts_b = build_counts(claim_b, bin_size)
+
+    if steal_times_b.size > 0:
+        ax.plot(
+            steal_times_b,
+            steal_counts_b,
+            linewidth=2,
+            color="#2980b9",
+            alpha=0.8,
+            label=f"{labels[1]} Steal",
+            linestyle="--",
+        )
+
+    if claim_times_b.size > 0:
+        ax.plot(
+            claim_times_b,
+            claim_counts_b,
+            linewidth=2,
+            color="#d35400",
+            alpha=0.8,
+            label=f"{labels[1]} Claim",
+            linestyle="--",
+        )
+
+    ax.legend(loc="upper right", frameon=True, fontsize=9, ncol=2)
+    ax.set_xlabel("Time (seconds)", fontsize=11)
+    ax.set_ylabel(f"Events per {bin_size}s", fontsize=11)
+    ax.set_title(
+        "ExtFrag Events Timeline (Solid = A, Dashed = B)",
+        fontsize=12,
+        fontweight="semibold",
+    )
+    ax.grid(True, alpha=0.06, linestyle=":", linewidth=0.5)
+
+
+def create_combined_migration_heatmap(ax, data_a, data_b, labels):
+    """Create combined migration pattern heatmap"""
+
+    events_a = [e for e in data_a.get("events", []) if e["event_type"] == "extfrag"]
+    events_b = [e for e in data_b.get("events", []) if e["event_type"] == "extfrag"]
+
+    if not events_a and not events_b:
+        ax.text(
+            0.5,
+            0.5,
+            "No external fragmentation events",
+            ha="center",
+            va="center",
+            fontsize=12,
+        )
+        ax.axis("off")
+        return
+
+    # Combine all events to get unified time range and patterns
+    all_events = events_a + events_b
+    times = [e["timestamp"] for e in all_events]
+    min_time, max_time = min(times), max(times)
+
+    # Create time bins
+    n_bins = min(25, max(15, int((max_time - min_time) / 10)))
+    time_bins = np.linspace(min_time, max_time, n_bins + 1)
+    time_centers = (time_bins[:-1] + time_bins[1:]) / 2
+
+    # Get all unique patterns from both datasets
+    all_patterns = set()
+    for e in all_events:
+        all_patterns.add(f"{e['migrate_from']}→{e['migrate_to']}")
+
+    # Calculate pattern severities and sort
+    pattern_severities = {}
+    for pattern in all_patterns:
+        from_type, to_type = pattern.split("→")
+        pattern_severities[pattern] = get_migration_severity(from_type, to_type)
+
+    sorted_patterns = sorted(all_patterns, key=lambda p: (pattern_severities[p], p))
+
+    # Create separate heatmaps for A and B
+    heatmap_a = np.zeros((len(sorted_patterns), len(time_centers)))
+    heatmap_b = np.zeros((len(sorted_patterns), len(time_centers)))
+
+    # Fill heatmap A
+    for e in events_a:
+        pattern = f"{e['migrate_from']}→{e['migrate_to']}"
+        pattern_idx = sorted_patterns.index(pattern)
+        bin_idx = np.digitize(e["timestamp"], time_bins) - 1
+        if 0 <= bin_idx < len(time_centers):
+            heatmap_a[pattern_idx, bin_idx] += 1
+
+    # Fill heatmap B
+    for e in events_b:
+        pattern = f"{e['migrate_from']}→{e['migrate_to']}"
+        pattern_idx = sorted_patterns.index(pattern)
+        bin_idx = np.digitize(e["timestamp"], time_bins) - 1
+        if 0 <= bin_idx < len(time_centers):
+            heatmap_b[pattern_idx, bin_idx] += 1
+
+    # Combine heatmaps: A in upper half of cell, B in lower half
+    from matplotlib.colors import LinearSegmentedColormap
+
+    # Plot base grid
+    for i in range(len(sorted_patterns)):
+        for j in range(len(time_centers)):
+            # Draw cell background based on severity
+            severity = pattern_severities[sorted_patterns[i]]
+            base_color = get_severity_color(severity)
+            rect = Rectangle(
+                (j - 0.5, i - 0.5),
+                1,
+                1,
+                facecolor=base_color,
+                alpha=0.1,
+                edgecolor="gray",
+                linewidth=0.5,
+            )
+            ax.add_patch(rect)
+
+            # Add counts for A (upper half)
+            if heatmap_a[i, j] > 0:
+                rect_a = Rectangle(
+                    (j - 0.4, i), 0.8, 0.4, facecolor="#3498db", alpha=0.6
+                )
+                ax.add_patch(rect_a)
+                ax.text(
+                    j,
+                    i + 0.2,
+                    str(int(heatmap_a[i, j])),
+                    ha="center",
+                    va="center",
+                    fontsize=6,
+                    color="white",
+                    fontweight="bold",
+                )
+
+            # Add counts for B (lower half)
+            if heatmap_b[i, j] > 0:
+                rect_b = Rectangle(
+                    (j - 0.4, i - 0.4), 0.8, 0.4, facecolor="#e67e22", alpha=0.6
+                )
+                ax.add_patch(rect_b)
+                ax.text(
+                    j,
+                    i - 0.2,
+                    str(int(heatmap_b[i, j])),
+                    ha="center",
+                    va="center",
+                    fontsize=6,
+                    color="white",
+                    fontweight="bold",
+                )
+
+    # Set axes - extend left margin for severity indicators
+    ax.set_xlim(-2.5, len(time_centers) - 0.5)
+    ax.set_ylim(-0.5, len(sorted_patterns) - 0.5)
+
+    # Set x-axis (time)
+    ax.set_xticks(np.arange(len(time_centers)))
+    ax.set_xticklabels(
+        [f"{t:.0f}s" for t in time_centers], rotation=45, ha="right", fontsize=8
+    )
+
+    # Set y-axis with severity indicators
+    ax.set_yticks(np.arange(len(sorted_patterns)))
+    y_labels = []
+
+    for i, pattern in enumerate(sorted_patterns):
+        severity = pattern_severities[pattern]
+
+        # Add colored severity indicator on the left side (in data coordinates)
+        severity_color = get_severity_color(severity)
+        rect = Rectangle(
+            (-1.8, i - 0.4),
+            1.0,
+            0.8,
+            facecolor=severity_color,
+            alpha=0.8,
+            edgecolor="black",
+            linewidth=0.5,
+            clip_on=False,
+        )
+        ax.add_patch(rect)
+
+        # Add severity symbol
+        if severity <= -2:
+            symbol = "!!"
+        elif severity <= -1:
+            symbol = "!"
+        elif severity >= 1:
+            symbol = "+"
+        else:
+            symbol = "="
+
+        ax.text(
+            -1.3,
+            i,
+            symbol,
+            ha="center",
+            va="center",
+            fontsize=8,
+            fontweight="bold",
+            color="white" if abs(severity) > 0 else "black",
+        )
+
+        y_labels.append(pattern)
+
+    ax.set_yticklabels(y_labels, fontsize=8)
+
+    # Add legend
+    legend_elements = [
+        mpatches.Patch(color="#3498db", alpha=0.6, label=f"{labels[0]} (upper)"),
+        mpatches.Patch(color="#e67e22", alpha=0.6, label=f"{labels[1]} (lower)"),
+        mpatches.Patch(color="#8b0000", alpha=0.8, label="Bad migration"),
+        mpatches.Patch(color="#51cf66", alpha=0.8, label="Good migration"),
+    ]
+    ax.legend(
+        handles=legend_elements,
+        loc="upper right",
+        bbox_to_anchor=(1.15, 1.0),
+        fontsize=8,
+        frameon=True,
+    )
+
+    # Styling
+    ax.set_xlabel("Time", fontsize=11)
+    ax.set_ylabel("Migration Pattern", fontsize=11)
+    ax.set_title(
+        "Migration Patterns Comparison (Blue = A, Orange = B)",
+        fontsize=12,
+        fontweight="semibold",
+    )
+    ax.grid(False)
+
+
+def create_comparison_statistics_table(ax, data_a, data_b, labels):
+    """Create comparison statistics table"""
+    ax.axis("off")
+
+    # Calculate metrics
+    def calculate_metrics(data):
+        events = data.get("events", [])
+        compact = [
+            e
+            for e in events
+            if e["event_type"] in ["compaction_success", "compaction_failure"]
+        ]
+        extfrag = [e for e in events if e["event_type"] == "extfrag"]
+
+        compact_success = sum(
+            1 for e in compact if e["event_type"] == "compaction_success"
+        )
+        success_rate = (compact_success / len(compact) * 100) if compact else 0
+
+        bad = sum(
+            1
+            for e in extfrag
+            if get_migration_severity(e["migrate_from"], e["migrate_to"]) < 0
+        )
+        good = sum(
+            1
+            for e in extfrag
+            if get_migration_severity(e["migrate_from"], e["migrate_to"]) > 0
+        )
+
+        steal = sum(1 for e in extfrag if e.get("is_steal"))
+        claim = len(extfrag) - steal if extfrag else 0
+
+        return {
+            "total": len(events),
+            "compact_success_rate": success_rate,
+            "extfrag": len(extfrag),
+            "bad_migrations": bad,
+            "good_migrations": good,
+            "steal": steal,
+            "claim": claim,
+        }
+
+    metrics_a = calculate_metrics(data_a)
+    metrics_b = calculate_metrics(data_b)
+
+    # Create table data
+    headers = ["Metric", labels[0], labels[1], "Better"]
+    rows = [
+        [
+            "Total Events",
+            metrics_a["total"],
+            metrics_b["total"],
+            "=" if metrics_a["total"] == metrics_b["total"] else "",
+        ],
+        [
+            "Compaction Success Rate",
+            f"{metrics_a['compact_success_rate']:.1f}%",
+            f"{metrics_b['compact_success_rate']:.1f}%",
+            (
+                labels[0]
+                if metrics_a["compact_success_rate"] > metrics_b["compact_success_rate"]
+                else (
+                    labels[1]
+                    if metrics_b["compact_success_rate"]
+                    > metrics_a["compact_success_rate"]
+                    else "="
+                )
+            ),
+        ],
+        [
+            "ExtFrag Events",
+            metrics_a["extfrag"],
+            metrics_b["extfrag"],
+            (
+                labels[0]
+                if metrics_a["extfrag"] < metrics_b["extfrag"]
+                else labels[1] if metrics_b["extfrag"] < metrics_a["extfrag"] else "="
+            ),
+        ],
+        [
+            "Bad Migrations",
+            metrics_a["bad_migrations"],
+            metrics_b["bad_migrations"],
+            (
+                labels[0]
+                if metrics_a["bad_migrations"] < metrics_b["bad_migrations"]
+                else (
+                    labels[1]
+                    if metrics_b["bad_migrations"] < metrics_a["bad_migrations"]
+                    else "="
+                )
+            ),
+        ],
+        [
+            "Good Migrations",
+            metrics_a["good_migrations"],
+            metrics_b["good_migrations"],
+            (
+                labels[0]
+                if metrics_a["good_migrations"] > metrics_b["good_migrations"]
+                else (
+                    labels[1]
+                    if metrics_b["good_migrations"] > metrics_a["good_migrations"]
+                    else "="
+                )
+            ),
+        ],
+        ["Steal Events", metrics_a["steal"], metrics_b["steal"], ""],
+        ["Claim Events", metrics_a["claim"], metrics_b["claim"], ""],
+    ]
+
+    # Create table - position closer to title
+    table = ax.table(
+        cellText=rows,
+        colLabels=headers,
+        cellLoc="center",
+        loc="center",
+        colWidths=[0.35, 0.25, 0.25, 0.15],
+        bbox=[0.1, 0.15, 0.8, 0.65],
+    )  # Center table with margins
+
+    table.auto_set_font_size(False)
+    table.set_fontsize(10)
+    table.scale(1, 2.2)  # Make cells taller for better readability
+
+    # Add padding to cells for better spacing
+    for key, cell in table.get_celld().items():
+        cell.set_height(0.08)  # Increase cell height
+        cell.PAD = 0.05  # Add internal padding
+
+    # Color cells based on which is better
+    for i in range(1, len(rows) + 1):
+        row = rows[i - 1]
+        if row[3] == labels[0]:
+            table[(i, 1)].set_facecolor("#d4edda")
+            table[(i, 2)].set_facecolor("#f8d7da")
+        elif row[3] == labels[1]:
+            table[(i, 1)].set_facecolor("#f8d7da")
+            table[(i, 2)].set_facecolor("#d4edda")
+
+    # Position title with more space from previous graph
+    ax.set_title(
+        "\n\nStatistical Comparison (Green = Better, Red = Worse)",
+        fontsize=13,
+        fontweight="bold",
+        pad=5,
+        y=1.0,
+    )
+
+
+def create_single_dashboard(data, output_file=None, bin_size=0.5):
+    """Create single dataset analysis dashboard with severity indicators"""
+
+    # Create figure
+    fig = plt.figure(figsize=(20, 16), constrained_layout=False)
+    fig.patch.set_facecolor("#f8f9fa")
+
+    # Create grid layout - 3 rows for single analysis
+    gs = gridspec.GridSpec(3, 1, height_ratios=[2.5, 2, 3], hspace=0.3, figure=fig)
+
+    # Create subplots
+    ax_compact = fig.add_subplot(gs[0])
+    ax_extfrag = fig.add_subplot(gs[1])
+    ax_migration = fig.add_subplot(gs[2])
+
+    # Process events
+    events = data.get("events", [])
+    compact_events = [
+        e
+        for e in events
+        if e["event_type"] in ["compaction_success", "compaction_failure"]
+    ]
+    extfrag_events = [e for e in events if e["event_type"] == "extfrag"]
+
+    success_events = [
+        e for e in compact_events if e["event_type"] == "compaction_success"
+    ]
+    failure_events = [
+        e for e in compact_events if e["event_type"] == "compaction_failure"
+    ]
+
+    # === COMPACTION GRAPH ===
+    if compact_events:
+        for e in success_events:
+            if e.get("fragmentation_index", 0) <= 1000:  # Cap at 1000
+                ax_compact.scatter(
+                    e["timestamp"],
+                    e.get("fragmentation_index", 0),
+                    s=get_dot_size(e["order"]),
+                    c="#2ecc71",
+                    alpha=0.3,
+                    edgecolors="none",
+                )
+
+        for i, e in enumerate(failure_events):
+            y_pos = -50 - (e["order"] * 10)
+            ax_compact.scatter(
+                e["timestamp"],
+                y_pos,
+                s=get_dot_size(e["order"]),
+                c="#e74c3c",
+                alpha=0.3,
+                edgecolors="none",
+            )
+
+        ax_compact.axhline(
+            y=0, color="#34495e", linestyle="-", linewidth=1.5, alpha=0.8
+        )
+        ax_compact.grid(True, alpha=0.08, linestyle=":", linewidth=0.5)
+        ax_compact.set_ylim(-200, 1000)
+
+        # Add statistics
+        success_rate = (
+            len(success_events) / len(compact_events) * 100 if compact_events else 0
+        )
+        stats_text = f"Success: {len(success_events)}/{len(compact_events)} ({success_rate:.1f}%)"
+        ax_compact.text(
+            0.02,
+            0.98,
+            stats_text,
+            transform=ax_compact.transAxes,
+            fontsize=10,
+            verticalalignment="top",
+            bbox=dict(boxstyle="round,pad=0.5", facecolor="white", alpha=0.9),
+        )
+
+    ax_compact.set_xlabel("Time (seconds)", fontsize=11)
+    ax_compact.set_ylabel("Fragmentation Index", fontsize=11)
+    ax_compact.set_title("Compaction Events Over Time", fontsize=13, fontweight="bold")
+
+    # === EXTFRAG TIMELINE ===
+    if extfrag_events:
+        steal_events = [e for e in extfrag_events if e.get("is_steal")]
+        claim_events = [e for e in extfrag_events if not e.get("is_steal")]
+
+        steal_times, steal_counts = build_counts(steal_events, bin_size)
+        claim_times, claim_counts = build_counts(claim_events, bin_size)
+
+        if steal_times.size > 0:
+            ax_extfrag.fill_between(
+                steal_times, 0, steal_counts, alpha=0.3, color="#3498db"
+            )
+            ax_extfrag.plot(
+                steal_times,
+                steal_counts,
+                linewidth=2,
+                color="#2980b9",
+                alpha=0.8,
+                label=f"Steal ({len(steal_events)})",
+            )
+
+        if claim_times.size > 0:
+            ax_extfrag.fill_between(
+                claim_times, 0, claim_counts, alpha=0.3, color="#e67e22"
+            )
+            ax_extfrag.plot(
+                claim_times,
+                claim_counts,
+                linewidth=2,
+                color="#d35400",
+                alpha=0.8,
+                label=f"Claim ({len(claim_events)})",
+            )
+
+        ax_extfrag.legend(loc="upper right", frameon=True, fontsize=9)
+
+        # Add bad/good migration counts
+        bad_migrations = sum(
+            1
+            for e in extfrag_events
+            if get_migration_severity(e["migrate_from"], e["migrate_to"]) < 0
+        )
+        good_migrations = sum(
+            1
+            for e in extfrag_events
+            if get_migration_severity(e["migrate_from"], e["migrate_to"]) > 0
+        )
+
+        migration_text = f"Bad: {bad_migrations} | Good: {good_migrations}"
+        ax_extfrag.text(
+            0.02,
+            0.98,
+            migration_text,
+            transform=ax_extfrag.transAxes,
+            fontsize=10,
+            verticalalignment="top",
+            bbox=dict(boxstyle="round,pad=0.5", facecolor="white", alpha=0.9),
+        )
+
+    ax_extfrag.set_xlabel("Time (seconds)", fontsize=11)
+    ax_extfrag.set_ylabel(f"Events per {bin_size}s", fontsize=11)
+    ax_extfrag.set_title(
+        "External Fragmentation Events Timeline", fontsize=12, fontweight="semibold"
+    )
+    ax_extfrag.grid(True, alpha=0.06, linestyle=":", linewidth=0.5)
+
+    # === MIGRATION HEATMAP WITH SEVERITY ===
+    create_single_migration_heatmap(ax_migration, extfrag_events)
+
+    # Super title
+    fig.suptitle(
+        "Memory Fragmentation Analysis", fontsize=18, fontweight="bold", y=0.98
+    )
+
+    # Footer
+    timestamp_text = f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
+    fig.text(
+        0.98,
+        0.01,
+        timestamp_text,
+        ha="right",
+        fontsize=9,
+        style="italic",
+        color="#7f8c8d",
+    )
+
+    # Adjust layout
+    plt.subplots_adjust(left=0.08, right=0.95, top=0.94, bottom=0.03)
+
+    # Save
+    if output_file is None:
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+        output_file = f"fragmentation_analysis_{timestamp}.png"
+
+    plt.savefig(output_file, dpi=120, bbox_inches="tight", facecolor="#f8f9fa")
+    plt.close(fig)
+    return output_file
+
+
+def create_single_migration_heatmap(ax, extfrag_events):
+    """Create migration heatmap for single dataset with severity indicators"""
+    if not extfrag_events:
+        ax.text(
+            0.5,
+            0.5,
+            "No external fragmentation events",
+            ha="center",
+            va="center",
+            fontsize=12,
+        )
+        ax.axis("off")
+        return
+
+    # Get time range and create bins
+    times = [e["timestamp"] for e in extfrag_events]
+    min_time, max_time = min(times), max(times)
+
+    n_bins = min(25, max(15, int((max_time - min_time) / 10)))
+    time_bins = np.linspace(min_time, max_time, n_bins + 1)
+    time_centers = (time_bins[:-1] + time_bins[1:]) / 2
+
+    # Get patterns and calculate severities
+    patterns = {}
+    pattern_events = defaultdict(list)
+
+    for e in extfrag_events:
+        pattern = f"{e['migrate_from']}→{e['migrate_to']}"
+        pattern_events[pattern].append(e)
+        if pattern not in patterns:
+            patterns[pattern] = {
+                "from": e["migrate_from"],
+                "to": e["migrate_to"],
+                "total": 0,
+                "steal": 0,
+                "claim": 0,
+                "severity": get_migration_severity(e["migrate_from"], e["migrate_to"]),
+            }
+        patterns[pattern]["total"] += 1
+        if e.get("is_steal"):
+            patterns[pattern]["steal"] += 1
+        else:
+            patterns[pattern]["claim"] += 1
+
+    # Sort by severity then count
+    sorted_patterns = sorted(
+        patterns.keys(), key=lambda p: (patterns[p]["severity"], -patterns[p]["total"])
+    )
+
+    # Create heatmap data
+    heatmap_data = np.zeros((len(sorted_patterns), len(time_centers)))
+
+    for i, pattern in enumerate(sorted_patterns):
+        for e in pattern_events[pattern]:
+            bin_idx = np.digitize(e["timestamp"], time_bins) - 1
+            if 0 <= bin_idx < len(time_centers):
+                heatmap_data[i, bin_idx] += 1
+
+    # Plot heatmap
+    from matplotlib.colors import LinearSegmentedColormap
+
+    colors = ["#ffffff", "#ffeb3b", "#ff9800", "#f44336"]  # White to red
+    cmap = LinearSegmentedColormap.from_list("intensity", colors, N=256)
+
+    im = ax.imshow(heatmap_data, aspect="auto", cmap=cmap, vmin=0, alpha=0.8)
+
+    # Overlay counts
+    for i in range(len(sorted_patterns)):
+        for j in range(len(time_centers)):
+            if heatmap_data[i, j] > 0:
+                count = int(heatmap_data[i, j])
+                color = (
+                    "white" if heatmap_data[i, j] > heatmap_data.max() / 2 else "black"
+                )
+                ax.text(
+                    j,
+                    i,
+                    str(count),
+                    ha="center",
+                    va="center",
+                    fontsize=6,
+                    fontweight="bold",
+                    color=color,
+                )
+
+    # Set axes
+    ax.set_xlim(-2.5, len(time_centers) - 0.5)
+    ax.set_ylim(-0.5, len(sorted_patterns) - 0.5)
+
+    # Set x-axis
+    ax.set_xticks(np.arange(len(time_centers)))
+    ax.set_xticklabels(
+        [f"{t:.0f}s" for t in time_centers], rotation=45, ha="right", fontsize=8
+    )
+
+    # Set y-axis with severity indicators
+    ax.set_yticks(np.arange(len(sorted_patterns)))
+    y_labels = []
+
+    for i, pattern in enumerate(sorted_patterns):
+        severity = patterns[pattern]["severity"]
+        severity_color = get_severity_color(severity)
+
+        # Add severity indicator
+        rect = Rectangle(
+            (-2.3, i - 0.4),
+            1.5,
+            0.8,
+            facecolor=severity_color,
+            alpha=0.8,
+            edgecolor="black",
+            linewidth=0.5,
+            clip_on=False,
+        )
+        ax.add_patch(rect)
+
+        # Add symbol
+        if severity <= -2:
+            symbol = "!!"
+        elif severity <= -1:
+            symbol = "!"
+        elif severity >= 1:
+            symbol = "+"
+        else:
+            symbol = "="
+
+        ax.text(
+            -1.55,
+            i,
+            symbol,
+            ha="center",
+            va="center",
+            fontsize=8,
+            fontweight="bold",
+            color="white" if abs(severity) > 0 else "black",
+        )
+
+        # Format label
+        total = patterns[pattern]["total"]
+        steal = patterns[pattern]["steal"]
+        claim = patterns[pattern]["claim"]
+        label = f"{pattern} ({total}: {steal}s/{claim}c)"
+        y_labels.append(label)
+
+    ax.set_yticklabels(y_labels, fontsize=8)
+
+    # Add colorbar
+    cbar = plt.colorbar(im, ax=ax, orientation="vertical", pad=0.02, aspect=30)
+    cbar.set_label("Event Intensity", fontsize=9)
+    cbar.ax.tick_params(labelsize=8)
+
+    # Add severity legend
+    bad_patch = mpatches.Patch(color="#8b0000", label="Bad (→UNMOVABLE)", alpha=0.8)
+    good_patch = mpatches.Patch(color="#51cf66", label="Good (→MOVABLE)", alpha=0.8)
+    neutral_patch = mpatches.Patch(color="#ffd43b", label="Neutral", alpha=0.8)
+
+    ax.legend(
+        handles=[bad_patch, neutral_patch, good_patch],
+        loc="upper right",
+        bbox_to_anchor=(1.15, 1.0),
+        title="Migration Impact",
+        fontsize=8,
+        title_fontsize=9,
+    )
+
+    # Styling
+    ax.set_xlabel("Time", fontsize=11)
+    ax.set_ylabel("Migration Pattern", fontsize=11)
+    ax.set_title(
+        "Migration Patterns Timeline with Severity Indicators",
+        fontsize=12,
+        fontweight="semibold",
+    )
+    ax.grid(False)
+
+    # Add grid lines
+    for i in range(len(sorted_patterns) + 1):
+        ax.axhline(i - 0.5, color="gray", linewidth=0.5, alpha=0.3)
+    for j in range(len(time_centers) + 1):
+        ax.axvline(j - 0.5, color="gray", linewidth=0.5, alpha=0.3)
+
+    # Summary
+    total_events = len(extfrag_events)
+    bad_events = sum(
+        patterns[p]["total"] for p in patterns if patterns[p]["severity"] < 0
+    )
+    good_events = sum(
+        patterns[p]["total"] for p in patterns if patterns[p]["severity"] > 0
+    )
+
+    summary = f"Total: {total_events} | Bad: {bad_events} | Good: {good_events}"
+    ax.text(
+        0.5,
+        -0.12,
+        summary,
+        transform=ax.transAxes,
+        ha="center",
+        fontsize=9,
+        style="italic",
+        color="#7f8c8d",
+    )
+
+
+def create_comparison_dashboard(data_a, data_b, labels, output_file=None):
+    """Create comprehensive comparison dashboard"""
+
+    # Create figure
+    fig = plt.figure(figsize=(20, 18), constrained_layout=False)
+    fig.patch.set_facecolor("#f8f9fa")
+
+    # Create grid layout - 4 rows, single column with more space for stats
+    gs = gridspec.GridSpec(
+        4, 1, height_ratios=[2.5, 2, 2.5, 1.5], hspace=0.45, figure=fig
+    )
+
+    # Create subplots
+    ax_compact = fig.add_subplot(gs[0])
+    ax_extfrag = fig.add_subplot(gs[1])
+    ax_migration = fig.add_subplot(gs[2])
+    ax_stats = fig.add_subplot(gs[3])
+
+    # Create visualizations
+    create_overlaid_compaction_graph(ax_compact, data_a, data_b, labels)
+    create_overlaid_extfrag_timeline(ax_extfrag, data_a, data_b, labels)
+    create_combined_migration_heatmap(ax_migration, data_a, data_b, labels)
+    create_comparison_statistics_table(ax_stats, data_a, data_b, labels)
+
+    # Super title
+    fig.suptitle(
+        "Memory Fragmentation A/B Comparison Analysis",
+        fontsize=18,
+        fontweight="bold",
+        y=0.98,
+    )
+
+    # Footer
+    timestamp_text = f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
+    fig.text(
+        0.98,
+        0.01,
+        timestamp_text,
+        ha="right",
+        fontsize=9,
+        style="italic",
+        color="#7f8c8d",
+    )
+
+    # Adjust layout with more bottom margin for stats table
+    plt.subplots_adjust(left=0.08, right=0.95, top=0.94, bottom=0.05)
+
+    # Save
+    if output_file is None:
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+        output_file = f"fragmentation_comparison_{timestamp}.png"
+
+    plt.savefig(output_file, dpi=120, bbox_inches="tight", facecolor="#f8f9fa")
+    plt.close(fig)
+    return output_file
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Fragmentation analysis with optional comparison"
+    )
+    parser.add_argument("input_file", help="Primary JSON file")
+    parser.add_argument(
+        "--compare", help="Secondary JSON file for A/B comparison (optional)"
+    )
+    parser.add_argument("-o", "--output", help="Output filename")
+    parser.add_argument(
+        "--labels",
+        nargs=2,
+        default=["Light Load", "Heavy Load"],
+        help="Labels for the two datasets in comparison mode",
+    )
+    parser.add_argument(
+        "--bin", type=float, default=0.5, help="Bin size for event counts"
+    )
+    args = parser.parse_args()
+
+    try:
+        data_a = load_data(args.input_file)
+    except Exception as e:
+        print(f"Error loading primary data: {e}")
+        sys.exit(1)
+
+    if args.compare:
+        # Comparison mode
+        try:
+            data_b = load_data(args.compare)
+        except Exception as e:
+            print(f"Error loading comparison data: {e}")
+            sys.exit(1)
+
+        out = create_comparison_dashboard(data_a, data_b, args.labels, args.output)
+        print(f"Comparison saved: {out}")
+    else:
+        # Single file mode
+        out = create_single_dashboard(data_a, args.output, args.bin)
+        print(f"Analysis saved: {out}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/playbooks/roles/monitoring/tasks/monitor_collect.yml b/playbooks/roles/monitoring/tasks/monitor_collect.yml
index 5432fc879bd0..f57a4e9d8106 100644
--- a/playbooks/roles/monitoring/tasks/monitor_collect.yml
+++ b/playbooks/roles/monitoring/tasks/monitor_collect.yml
@@ -206,3 +206,129 @@
     - monitor_developmental_stats|default(false)|bool
     - monitor_folio_migration|default(false)|bool
     - folio_migration_data_file.stat.exists|default(false)
+
+# Plot-fragmentation collection tasks
+- name: Check if fragmentation monitoring was started
+  become: true
+  become_method: sudo
+  ansible.builtin.stat:
+    path: "{{ monitor_fragmentation_output_dir|default('/root/monitoring/fragmentation') }}/fragmentation_tracker.pid"
+  register: fragmentation_pid_file
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+
+- name: Stop fragmentation monitoring
+  become: true
+  become_method: sudo
+  ansible.builtin.shell: |
+    output_dir="{{ monitor_fragmentation_output_dir|default('/root/monitoring/fragmentation') }}"
+    if [ -f "${output_dir}/fragmentation_tracker.pid" ]; then
+      pid=$(cat "${output_dir}/fragmentation_tracker.pid")
+      if ps -p $pid > /dev/null 2>&1; then
+        kill -SIGINT $pid  # Use SIGINT to allow graceful shutdown
+        sleep 2  # Give it time to save data
+        if ps -p $pid > /dev/null 2>&1; then
+          kill -SIGTERM $pid  # Force kill if still running
+        fi
+        echo "Stopped fragmentation monitoring process $pid"
+      else
+        echo "Fragmentation monitoring process $pid was not running"
+      fi
+      rm -f "${output_dir}/fragmentation_tracker.pid"
+    fi
+
+    # Save the end time
+    date +"%Y-%m-%d %H:%M:%S" > "${output_dir}/end_time.txt"
+  register: stop_fragmentation_monitor
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+    - fragmentation_pid_file.stat.exists|default(false)
+
+- name: Display stop fragmentation monitoring status
+  ansible.builtin.debug:
+    msg: "{{ stop_fragmentation_monitor.stdout }}"
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+    - stop_fragmentation_monitor is defined
+    - stop_fragmentation_monitor.changed|default(false)
+
+- name: Generate fragmentation visualization
+  become: true
+  become_method: sudo
+  ansible.builtin.shell: |
+    cd /opt/fragmentation
+    output_dir="{{ monitor_fragmentation_output_dir|default('/root/monitoring/fragmentation') }}"
+
+    # Run the visualizer if data exists
+    if [ -f "${output_dir}/fragmentation_data.json" ] || [ -f "${output_dir}/fragmentation_tracker.log" ]; then
+      python3 fragmentation_visualizer.py \
+        --input "${output_dir}" \
+        --output "${output_dir}/fragmentation_plot.png" 2>&1 | tee "${output_dir}/visualizer.log"
+      echo "Generated fragmentation visualization"
+    else
+      echo "No fragmentation data found to visualize"
+    fi
+  register: generate_fragmentation_plot
+  ignore_errors: true
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+
+- name: List fragmentation monitoring output files
+  become: true
+  become_method: sudo
+  ansible.builtin.find:
+    paths: "{{ monitor_fragmentation_output_dir|default('/root/monitoring/fragmentation') }}"
+    patterns: "*"
+    file_type: file
+  register: fragmentation_output_files
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+
+- name: Create local fragmentation results directory
+  ansible.builtin.file:
+    path: "{{ monitoring_results_path }}/fragmentation"
+    state: directory
+  delegate_to: localhost
+  run_once: true
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+    - fragmentation_output_files.files is defined
+    - fragmentation_output_files.files | length > 0
+
+- name: Copy fragmentation monitoring data to localhost
+  become: true
+  become_method: sudo
+  ansible.builtin.fetch:
+    src: "{{ item.path }}"
+    dest: "{{ monitoring_results_path }}/fragmentation/{{ ansible_hostname }}_{{ item.path | basename }}"
+    flat: true
+    validate_checksum: false
+  loop: "{{ fragmentation_output_files.files | default([]) }}"
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+    - fragmentation_output_files.files is defined
+
+- name: Display fragmentation monitoring collection summary
+  ansible.builtin.debug:
+    msg: |
+      Fragmentation monitoring collection complete.
+      {% if fragmentation_output_files.files is defined and fragmentation_output_files.files | length > 0 %}
+      Collected {{ fragmentation_output_files.files | length }} files
+      Data saved to: {{ monitoring_results_path }}/fragmentation/
+      Files collected:
+      {% for file in fragmentation_output_files.files %}
+        - {{ ansible_hostname }}_{{ file.path | basename }}
+      {% endfor %}
+      {% else %}
+      No fragmentation data was collected.
+      {% endif %}
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
diff --git a/playbooks/roles/monitoring/tasks/monitor_run.yml b/playbooks/roles/monitoring/tasks/monitor_run.yml
index f56d06e4facf..c563b38bc0b5 100644
--- a/playbooks/roles/monitoring/tasks/monitor_run.yml
+++ b/playbooks/roles/monitoring/tasks/monitor_run.yml
@@ -81,3 +81,126 @@
     - monitor_folio_migration|default(false)|bool
     - folio_migration_stats_file.stat.exists|default(false)
     - monitor_status is defined
+
+# Plot-fragmentation monitoring tasks
+- name: Install python3-bpfcc for fragmentation monitoring
+  become: true
+  become_method: sudo
+  ansible.builtin.package:
+    name: python3-bpfcc
+    state: present
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+
+- name: Install matplotlib for fragmentation visualization
+  become: true
+  become_method: sudo
+  ansible.builtin.pip:
+    name: matplotlib
+    state: present
+    executable: pip3
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+
+- name: Create fragmentation scripts directory
+  become: true
+  become_method: sudo
+  ansible.builtin.file:
+    path: /opt/fragmentation
+    state: directory
+    mode: "0755"
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+
+- name: Copy fragmentation monitoring scripts to target
+  become: true
+  become_method: sudo
+  ansible.builtin.copy:
+    src: "{{ item }}"
+    dest: "/opt/fragmentation/{{ item | basename }}"
+    mode: "0755"
+  loop:
+    - "{{ playbook_dir }}/roles/monitoring/files/fragmentation_tracker.py"
+    - "{{ playbook_dir }}/roles/monitoring/files/fragmentation_visualizer.py"
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+
+- name: Create fragmentation monitoring output directory
+  become: true
+  become_method: sudo
+  ansible.builtin.file:
+    path: "{{ monitor_fragmentation_output_dir|default('/root/monitoring/fragmentation') }}"
+    state: directory
+    mode: "0755"
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+
+- name: Start fragmentation monitoring in background
+  become: true
+  become_method: sudo
+  ansible.builtin.shell: |
+    cd /opt/fragmentation
+    duration="{{ monitor_fragmentation_duration|default(0) }}"
+    output_dir="{{ monitor_fragmentation_output_dir|default('/root/monitoring/fragmentation') }}"
+
+    # Start the fragmentation tracker
+    if [ "$duration" -eq "0" ]; then
+      # Run continuously until killed
+      nohup python3 fragmentation_tracker.py > "${output_dir}/fragmentation_tracker.log" 2>&1 &
+    else
+      # Run for specified duration
+      nohup timeout ${duration} python3 fragmentation_tracker.py > "${output_dir}/fragmentation_tracker.log" 2>&1 &
+    fi
+    echo $! > "${output_dir}/fragmentation_tracker.pid"
+
+    # Also save the start time for reference
+    date +"%Y-%m-%d %H:%M:%S" > "${output_dir}/start_time.txt"
+  async: 86400 # Run for up to 24 hours
+  poll: 0
+  register: fragmentation_monitor
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+
+- name: Save fragmentation monitor async job ID
+  ansible.builtin.set_fact:
+    fragmentation_monitor_job: "{{ fragmentation_monitor.ansible_job_id }}"
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+    - fragmentation_monitor is defined
+
+- name: Verify fragmentation monitoring started successfully
+  become: true
+  become_method: sudo
+  ansible.builtin.shell: |
+    output_dir="{{ monitor_fragmentation_output_dir|default('/root/monitoring/fragmentation') }}"
+    if [ -f "${output_dir}/fragmentation_tracker.pid" ]; then
+      pid=$(cat "${output_dir}/fragmentation_tracker.pid")
+      if ps -p $pid > /dev/null 2>&1; then
+        echo "Fragmentation monitoring process $pid is running"
+      else
+        echo "ERROR: Fragmentation monitoring process $pid is not running" >&2
+        exit 1
+      fi
+    else
+      echo "ERROR: PID file not found" >&2
+      exit 1
+    fi
+  register: fragmentation_monitor_status
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+
+- name: Display fragmentation monitoring status
+  ansible.builtin.debug:
+    msg: "{{ fragmentation_monitor_status.stdout }}"
+  when:
+    - monitor_developmental_stats|default(false)|bool
+    - monitor_memory_fragmentation|default(false)|bool
+    - fragmentation_monitor_status is defined
-- 
2.45.2


  reply	other threads:[~2025-09-04  9:13 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-09-04  9:13 [PATCH 0/5] add memory fragmentation automation testing Luis Chamberlain
2025-09-04  9:13 ` Luis Chamberlain [this message]
2025-09-04  9:13 ` [PATCH 2/5] mmtests: add monitoring framework integration Luis Chamberlain
2025-09-04  9:13 ` [PATCH 3/5] sysbench: " Luis Chamberlain
2025-09-04  9:13 ` [PATCH 4/5] ai milvus: add monitoring support Luis Chamberlain
2025-09-04  9:13 ` [PATCH 5/5] minio: " Luis Chamberlain
2025-09-19  3:49 ` [PATCH 0/5] add memory fragmentation automation testing Luis Chamberlain

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250904091322.2499058-2-mcgrof@kernel.org \
    --to=mcgrof@kernel.org \
    --cc=cel@kernel.org \
    --cc=da.gomez@kruces.com \
    --cc=kdevops@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox