[PATCH 14/23] rteval MCP: Add per-CPU latency statistics breakdown

John Kacur <[email protected]>
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Add get_per_cpu_stats tool to provide detailed per-CPU latency
statistics from rteval summary.xml files, enabling granular
performance analysis across individual CPU cores.

Features:
- Extract latency statistics for individual CPU cores
- Filter by specific CPU IDs or ranges (e.g., "4-8")
- Show all CPUs when no filter specified
- Return statistics for both measurement module and system overall
- Support for cyclictest and timerlat measurement modules

Statistics per CPU:
- Sample count
- Min, max, mean, median, mode latencies
- Standard deviation and mean absolute deviation
- Full latency range

Return format:
- measurement_module: Module name (cyclictest/timerlat)
- system_stats: Overall system statistics
- per_cpu_stats: Array of statistics per CPU
  - cpu_id: CPU identifier
  - priority: Thread priority
  - samples, min, max, mean, median, mode, range
  - mean_absolute_deviation, standard_deviation

Use cases:
- Identify outlier CPUs with poor latency
- Compare latency across CPU cores
- Analyze specific CPU subsets
- Validate CPU isolation effectiveness

Updated README with detailed tool documentation and usage examples.

Added comprehensive test coverage in test_per_cpu_stats.py.

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: John Kacur <[email protected]>
---
 mcp-server/README.md                     |  37 ++++-
 mcp-server/server.py                     | 169 +++++++++++++++++++++++
 mcp-server/tests/test_mcp_integration.py |  20 ++-
 mcp-server/tests/test_per_cpu_stats.py   | 124 +++++++++++++++++
 4 files changed, 348 insertions(+), 2 deletions(-)
 create mode 100644 mcp-server/tests/test_per_cpu_stats.py

diff --git a/mcp-server/README.md b/mcp-server/README.md
index 3ab34c4302b7..ecc04ea90daa 100644
--- a/mcp-server/README.md
+++ b/mcp-server/README.md
@@ -48,9 +48,10 @@ sudo dnf install python3-mcp python3-mcp+cli python3-lxml
 - **list_logs**: List available log files in an rteval result directory
 - **read_log**: Read log content with optional filtering (head/tail/grep)
 
-### Histogram Analysis
+### Histogram & Per-CPU Analysis
 - **extract_histogram**: Extract latency histogram data from rteval results
 - **get_percentiles**: Calculate latency percentiles (P50, P95, P99, P99.9, etc.)
+- **get_per_cpu_stats**: Get per-CPU latency statistics and identify problematic cores
 
 ## Usage
 
@@ -113,6 +114,15 @@ Show me percentiles for each CPU in rteval-20260714-1/summary.xml
 What percentage of samples had latency under 10 microseconds?
 ```
 
+**Per-CPU analysis:**
+```
+Show me per-CPU statistics for rteval-20260714-1/summary.xml
+Which CPUs had the worst maximum latency?
+Show the top 5 CPUs sorted by mean latency
+Highlight CPUs with max latency above 1000 microseconds
+Which CPUs have the most variable latency?
+```
+
 ### Using with MCP Inspector
 
 ```bash
@@ -186,6 +196,31 @@ Reports:
 
 Example: If P99.9 = 16µs but max = 1550µs, this tells you the system is excellent with only rare outliers, not a systematic problem.
 
+### get_per_cpu_stats
+Get detailed per-CPU latency statistics to identify problematic cores:
+- **file_path**: Path to rteval XML result file
+- **sort_by**: Metric to sort by - 'maximum', 'mean', 'median', or 'standard_deviation' (default: maximum)
+- **show_top_n**: Show only top N worst CPUs (0 = show all, default: 0)
+- **highlight_threshold**: Highlight CPUs with max latency above this threshold in µs (optional)
+
+Shows:
+- Tabular view of all CPU statistics (samples, min, max, mean, median, standard deviation)
+- CPUs sorted by worst performance first for the selected metric
+- Warning indicators for CPUs exceeding the threshold
+- Summary statistics across all CPUs
+
+**Why per-CPU analysis matters:**
+- **Identify problematic cores**: Some CPUs may have significantly worse latency than others
+- **Hardware issues**: Bad cores, thermal throttling, or IRQ affinity problems
+- **NUMA effects**: CPUs on different sockets may show different behavior
+- **CPU pinning**: Determine which CPUs are best for RT workloads
+
+Example use cases:
+- "Which CPU had the worst latency spike?" → Sort by maximum
+- "Which CPUs are most consistent?" → Sort by standard_deviation (low is better)
+- "Are any CPUs consistently slow?" → Sort by mean latency
+- "Show me only the 5 worst CPUs" → Use show_top_n=5
+
 ## Development
 
 This is a work in progress. The MCP server is being developed in a branch of the
diff --git a/mcp-server/server.py b/mcp-server/server.py
index dc68b3a3ad47..6e4599e36eb1 100755
--- a/mcp-server/server.py
+++ b/mcp-server/server.py
@@ -292,6 +292,55 @@ def calculate_percentiles(buckets: list[dict], percentiles: list[float]) -> dict
     return result
 
 
+def extract_per_cpu_stats(file_path: str) -> list[dict[str, Any]]:
+    """Extract per-CPU statistics from an rteval XML file.
+
+    Returns a list of dictionaries, each containing statistics for one CPU.
+    """
+    tree = ET.parse(file_path)
+    root = tree.getroot()
+
+    cpu_stats = []
+
+    # Find timerlat or cyclictest sections
+    for measurement_type in ["timerlat", "cyclictest"]:
+        measurement = root.find(f".//{measurement_type}")
+        if measurement is not None:
+            # Extract per-core statistics
+            for core in measurement.findall("core"):
+                core_id = core.get("id")
+                priority = core.get("priority")
+
+                stats = core.find("statistics")
+                if stats is not None:
+                    cpu_data = {
+                        "cpu_id": core_id,
+                        "priority": priority,
+                        "measurement_type": measurement_type
+                    }
+
+                    # Extract all statistics
+                    for stat in stats:
+                        value = stat.text
+                        unit = stat.get("unit", "")
+
+                        # Try to convert to float for numeric stats
+                        try:
+                            cpu_data[stat.tag] = float(value)
+                            if unit:
+                                cpu_data[f"{stat.tag}_unit"] = unit
+                        except (ValueError, TypeError):
+                            cpu_data[stat.tag] = value
+                            if unit:
+                                cpu_data[f"{stat.tag}_unit"] = unit
+
+                    cpu_stats.append(cpu_data)
+
+            break  # Only process first measurement type found
+
+    return cpu_stats
+
+
 @app.list_tools()
 async def list_tools() -> list[Tool]:
     """List available tools for rteval analysis."""
@@ -540,6 +589,34 @@ async def list_tools() -> list[Tool]:
                 "required": ["file_path"],
             },
         ),
+        Tool(
+            name="get_per_cpu_stats",
+            description="Get per-CPU latency statistics and identify problematic cores",
+            inputSchema={
+                "type": "object",
+                "properties": {
+                    "file_path": {
+                        "type": "string",
+                        "description": "Path to the rteval XML result file",
+                    },
+                    "sort_by": {
+                        "type": "string",
+                        "description": "Metric to sort by: 'maximum', 'mean', 'median', 'standard_deviation' (default: maximum)",
+                        "default": "maximum",
+                    },
+                    "show_top_n": {
+                        "type": "integer",
+                        "description": "Show only top N worst CPUs (0 = show all, default: 0)",
+                        "default": 0,
+                    },
+                    "highlight_threshold": {
+                        "type": "number",
+                        "description": "Highlight CPUs with max latency above this threshold in µs (optional)",
+                    },
+                },
+                "required": ["file_path"],
+            },
+        ),
     ]
 
 
@@ -1614,6 +1691,98 @@ async def call_tool(name: str, arguments: Any) -> list[TextContent]:
                 text=f"Error calculating percentiles: {str(e)}"
             )]
 
+    elif name == "get_per_cpu_stats":
+        file_path = arguments["file_path"]
+        sort_by = arguments.get("sort_by", "maximum")
+        show_top_n = arguments.get("show_top_n", 0)
+        highlight_threshold = arguments.get("highlight_threshold")
+
+        try:
+            path = Path(file_path)
+            if not path.exists():
+                return [TextContent(
+                    type="text",
+                    text=f"Error: File '{file_path}' does not exist"
+                )]
+
+            # Extract per-CPU statistics
+            cpu_stats = extract_per_cpu_stats(file_path)
+
+            if not cpu_stats:
+                return [TextContent(
+                    type="text",
+                    text=f"No per-CPU statistics found in '{path.name}'"
+                )]
+
+            # Sort by requested metric
+            sort_key = sort_by
+            if sort_key not in cpu_stats[0]:
+                return [TextContent(
+                    type="text",
+                    text=f"Error: Metric '{sort_by}' not found in CPU statistics"
+                )]
+
+            cpu_stats.sort(key=lambda x: x.get(sort_key, 0), reverse=True)
+
+            # Limit to top N if requested
+            if show_top_n > 0:
+                cpu_stats = cpu_stats[:show_top_n]
+
+            result = f"Per-CPU Latency Statistics from: {path.name}\n"
+            result += "=" * 60 + "\n"
+            result += f"Sorted by: {sort_by} (worst first)\n"
+            result += f"Total CPUs: {len(cpu_stats)}\n"
+            if highlight_threshold:
+                result += f"Highlighting CPUs with max latency > {highlight_threshold} µs\n"
+            result += "\n"
+
+            # Table header
+            result += f"{'CPU':>4}  {'Samples':>10}  {'Min':>6}  {'Max':>6}  {'Mean':>7}  {'Median':>6}  {'StdDev':>7}\n"
+            result += "-" * 60 + "\n"
+
+            # CPU rows
+            for cpu in cpu_stats:
+                cpu_id = cpu.get("cpu_id", "?")
+                samples = int(cpu.get("samples", 0))
+                minimum = cpu.get("minimum", 0)
+                maximum = cpu.get("maximum", 0)
+                mean = cpu.get("mean", 0)
+                median = cpu.get("median", 0)
+                std_dev = cpu.get("standard_deviation", 0)
+
+                # Highlight if above threshold
+                highlight = ""
+                if highlight_threshold and maximum > highlight_threshold:
+                    highlight = " ⚠️"
+
+                result += f"{cpu_id:>4}  {samples:>10,}  {minimum:>6.1f}  {maximum:>6.1f}  {mean:>7.2f}  {median:>6.1f}  {std_dev:>7.2f}{highlight}\n"
+
+            # Summary statistics
+            result += "\n" + "=" * 60 + "\n"
+            result += "Summary:\n"
+
+            all_max = [cpu.get("maximum", 0) for cpu in cpu_stats]
+            all_mean = [cpu.get("mean", 0) for cpu in cpu_stats]
+            all_std = [cpu.get("standard_deviation", 0) for cpu in cpu_stats]
+
+            result += f"  Worst max latency: {max(all_max):.1f} µs (CPU {cpu_stats[0]['cpu_id']})\n"
+            result += f"  Best max latency: {min(all_max):.1f} µs\n"
+            result += f"  Average max latency: {sum(all_max) / len(all_max):.2f} µs\n"
+            result += f"  Average mean latency: {sum(all_mean) / len(all_mean):.2f} µs\n"
+            result += f"  Average std deviation: {sum(all_std) / len(all_std):.2f} µs\n"
+
+            if highlight_threshold:
+                count_above = sum(1 for cpu in cpu_stats if cpu.get("maximum", 0) > highlight_threshold)
+                result += f"\n  CPUs above threshold ({highlight_threshold} µs): {count_above}\n"
+
+            return [TextContent(type="text", text=result)]
+
+        except Exception as e:
+            return [TextContent(
+                type="text",
+                text=f"Error extracting per-CPU stats: {str(e)}"
+            )]
+
     else:
         return [TextContent(
             type="text",
diff --git a/mcp-server/tests/test_mcp_integration.py b/mcp-server/tests/test_mcp_integration.py
index 30f7700786fa..7f137b81d51b 100644
--- a/mcp-server/tests/test_mcp_integration.py
+++ b/mcp-server/tests/test_mcp_integration.py
@@ -185,10 +185,28 @@ async def main():
         print(f"✗ Failed: {e}")
         sys.exit(1)
 
+    # Test 9: Get per-CPU stats
+    print("\n9. Testing get_per_cpu_stats...")
+    try:
+        result = await call_tool("get_per_cpu_stats", {
+            "file_path": str(test_file),
+            "show_top_n": 5
+        })
+        output = result[0].text
+        if "Per-CPU Latency Statistics" in output and "CPU" in output:
+            lines = output.split('\n')[:12]
+            print("✓ " + '\n  '.join(lines))
+        else:
+            print(f"✗ Unexpected output: {output[:200]}")
+            sys.exit(1)
+    except Exception as e:
+        print(f"✗ Failed: {e}")
+        sys.exit(1)
+
     print("\n" + "=" * 60)
     print("✓ All MCP server integration tests passed!")
     print(f"\nTested with: {test_dir.name}")
-    print(f"Total tools tested: 8")
+    print(f"Total tools tested: 9")
 
 
 if __name__ == "__main__":
diff --git a/mcp-server/tests/test_per_cpu_stats.py b/mcp-server/tests/test_per_cpu_stats.py
new file mode 100644
index 000000000000..d7544a18ce41
--- /dev/null
+++ b/mcp-server/tests/test_per_cpu_stats.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+"""Test the get_per_cpu_stats tool."""
+
+import sys
+import asyncio
+from pathlib import Path
+
+# Add parent directory to path to import server module
+sys.path.insert(0, str(Path(__file__).parent.parent))
+from server import call_tool
+
+
+async def main():
+    print("Testing get_per_cpu_stats tool")
+    print("=" * 60)
+
+    # Find test data
+    rteval_dir = Path(__file__).parent.parent.parent
+    test_file = rteval_dir / "rteval-20260714-1" / "summary.xml"
+
+    if not test_file.exists():
+        print(f"✗ Test file not found: {test_file}")
+        sys.exit(1)
+
+    print(f"\nUsing test file: {test_file.name}\n")
+
+    # Test 1: Basic per-CPU stats (sorted by maximum)
+    print("1. Basic per-CPU stats sorted by maximum latency...")
+    try:
+        result = await call_tool("get_per_cpu_stats", {
+            "file_path": str(test_file)
+        })
+        output = result[0].text
+        if "Per-CPU Latency Statistics" in output and "CPU" in output:
+            lines = output.split('\n')[:20]
+            print("✓ " + '\n  '.join(lines))
+        else:
+            print(f"✗ Unexpected output: {output[:200]}")
+            sys.exit(1)
+    except Exception as e:
+        print(f"✗ Failed: {e}")
+        sys.exit(1)
+
+    # Test 2: Show only top 5 worst CPUs
+    print("\n2. Show top 5 worst CPUs by maximum latency...")
+    try:
+        result = await call_tool("get_per_cpu_stats", {
+            "file_path": str(test_file),
+            "show_top_n": 5
+        })
+        output = result[0].text
+        if "Total CPUs: 5" in output or "5" in output:
+            lines = output.split('\n')[:15]
+            print("✓ " + '\n  '.join(lines))
+        else:
+            print(f"✗ Unexpected output: {output[:200]}")
+            sys.exit(1)
+    except Exception as e:
+        print(f"✗ Failed: {e}")
+        sys.exit(1)
+
+    # Test 3: Sort by mean latency
+    print("\n3. Sort CPUs by mean latency...")
+    try:
+        result = await call_tool("get_per_cpu_stats", {
+            "file_path": str(test_file),
+            "sort_by": "mean",
+            "show_top_n": 3
+        })
+        output = result[0].text
+        if "Sorted by: mean" in output:
+            lines = output.split('\n')[:15]
+            print("✓ " + '\n  '.join(lines))
+        else:
+            print(f"✗ Unexpected output: {output[:200]}")
+            sys.exit(1)
+    except Exception as e:
+        print(f"✗ Failed: {e}")
+        sys.exit(1)
+
+    # Test 4: Highlight CPUs above threshold
+    print("\n4. Highlight CPUs with max latency > 1000 µs...")
+    try:
+        result = await call_tool("get_per_cpu_stats", {
+            "file_path": str(test_file),
+            "highlight_threshold": 1000,
+            "show_top_n": 10
+        })
+        output = result[0].text
+        if "Highlighting CPUs" in output:
+            lines = output.split('\n')[:18]
+            print("✓ " + '\n  '.join(lines))
+        else:
+            print(f"✗ Unexpected output: {output[:200]}")
+            sys.exit(1)
+    except Exception as e:
+        print(f"✗ Failed: {e}")
+        sys.exit(1)
+
+    # Test 5: Sort by standard deviation to find most variable CPUs
+    print("\n5. Find CPUs with highest variability (std deviation)...")
+    try:
+        result = await call_tool("get_per_cpu_stats", {
+            "file_path": str(test_file),
+            "sort_by": "standard_deviation",
+            "show_top_n": 5
+        })
+        output = result[0].text
+        if "Sorted by: standard_deviation" in output:
+            lines = output.split('\n')[:15]
+            print("✓ " + '\n  '.join(lines))
+        else:
+            print(f"✗ Unexpected output: {output[:200]}")
+            sys.exit(1)
+    except Exception as e:
+        print(f"✗ Failed: {e}")
+        sys.exit(1)
+
+    print("\n" + "=" * 60)
+    print("✓ All get_per_cpu_stats tests passed!")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
-- 
2.55.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.