[PATCH 02/23] rteval: Implement compare_results functionality in MCP server

John Kacur <[email protected]>
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Add full comparison logic to the compare_results tool. The tool now:

- Extracts structured data from both rteval XML files
- Compares system information (kernel, OS, architecture)
- Compares run durations
- Compares timerlat/cyclictest statistics with detailed metrics
- Calculates differences and percentage changes
- Handles both timerlat and cyclictest measurement types

Added helper function extract_rteval_data() to parse XML files
and extract key information in a structured format for easy comparison.

Tested with two rteval 3.11 runs showing consistent results:
- Maximum latency: 3499 vs 3495 us (-0.11%)
- Mean latency: 4.22 vs 4.22 us (+0.09%)

This enables users to track performance trends and identify regressions
across multiple test runs.

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: John Kacur <[email protected]>
---
 mcp-server/server.py | 200 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 194 insertions(+), 6 deletions(-)

diff --git a/mcp-server/server.py b/mcp-server/server.py
index a3ca9e3c5783..40c11dfecb34 100755
--- a/mcp-server/server.py
+++ b/mcp-server/server.py
@@ -21,6 +21,75 @@ from mcp.types import Tool, TextContent
 app = Server("rteval-mcp-server")
 
 
+def extract_rteval_data(file_path: str) -> dict[str, Any]:
+    """Extract key data from an rteval XML file for comparison."""
+    tree = ET.parse(file_path)
+    root = tree.getroot()
+
+    data = {
+        "file": file_path,
+        "rteval_version": root.get("version", "unknown"),
+        "run_info": {},
+        "system_info": {},
+        "measurements": {}
+    }
+
+    # Run information
+    runinfo = root.find("run_info")
+    if runinfo is not None:
+        data["run_info"] = {
+            "days": int(runinfo.get("days", "0")),
+            "hours": int(runinfo.get("hours", "0")),
+            "minutes": int(runinfo.get("minutes", "0")),
+            "seconds": int(runinfo.get("seconds", "0")),
+        }
+        date_elem = runinfo.find("date")
+        time_elem = runinfo.find("time")
+        if date_elem is not None and time_elem is not None:
+            data["run_info"]["date"] = date_elem.text
+            data["run_info"]["time"] = time_elem.text
+
+    # System information
+    sysinfo = root.find("SystemInfo")
+    if sysinfo is not None:
+        uname = sysinfo.find("uname")
+        if uname is not None:
+            for elem in ["baseos", "node", "arch", "kernel"]:
+                node = uname.find(elem)
+                if node is not None:
+                    if elem == "kernel":
+                        data["system_info"]["kernel"] = node.text
+                        data["system_info"]["is_RT"] = node.get("is_RT", "0") == "1"
+                    else:
+                        data["system_info"][elem] = node.text
+
+    # Timerlat measurements
+    timerlat = root.find(".//timerlat")
+    if timerlat is not None:
+        data["measurements"]["timerlat"] = {}
+        stats = timerlat.find(".//statistics")
+        if stats is not None:
+            for stat in stats:
+                data["measurements"]["timerlat"][stat.tag] = {
+                    "value": stat.text,
+                    "unit": stat.get("unit", "")
+                }
+
+    # Cyclictest measurements
+    cyclictest = root.find(".//cyclictest")
+    if cyclictest is not None:
+        data["measurements"]["cyclictest"] = {}
+        stats = cyclictest.find(".//statistics")
+        if stats is not None:
+            for stat in stats:
+                data["measurements"]["cyclictest"][stat.tag] = {
+                    "value": stat.text,
+                    "unit": stat.get("unit", "")
+                }
+
+    return data
+
+
 @app.list_tools()
 async def list_tools() -> list[Tool]:
     """List available tools for rteval analysis."""
@@ -227,12 +296,131 @@ async def call_tool(name: str, arguments: Any) -> list[TextContent]:
         file1 = arguments["file1"]
         file2 = arguments["file2"]
 
-        # For now, just return a placeholder
-        # This would need actual comparison logic based on rteval structure
-        return [TextContent(
-            type="text",
-            text=f"Comparison tool not yet implemented.\nWould compare:\n  {file1}\n  {file2}\n"
-        )]
+        try:
+            # Check if files exist
+            if not Path(file1).exists():
+                return [TextContent(
+                    type="text",
+                    text=f"Error: File '{file1}' does not exist"
+                )]
+            if not Path(file2).exists():
+                return [TextContent(
+                    type="text",
+                    text=f"Error: File '{file2}' does not exist"
+                )]
+
+            # Extract data from both files
+            data1 = extract_rteval_data(file1)
+            data2 = extract_rteval_data(file2)
+
+            # Build comparison report
+            result = "rteval Results Comparison\n"
+            result += "=" * 60 + "\n\n"
+
+            result += f"File 1: {Path(file1).name}\n"
+            result += f"File 2: {Path(file2).name}\n\n"
+
+            # Compare system information
+            result += "System Comparison:\n"
+            result += "-" * 60 + "\n"
+
+            sys1 = data1.get("system_info", {})
+            sys2 = data2.get("system_info", {})
+
+            for key in ["baseos", "node", "arch", "kernel", "is_RT"]:
+                val1 = sys1.get(key, "N/A")
+                val2 = sys2.get(key, "N/A")
+                if val1 != val2:
+                    result += f"  {key}:\n"
+                    result += f"    File 1: {val1}\n"
+                    result += f"    File 2: {val2}\n"
+                else:
+                    result += f"  {key}: {val1}\n"
+            result += "\n"
+
+            # Compare run durations
+            result += "Run Duration:\n"
+            result += "-" * 60 + "\n"
+
+            run1 = data1.get("run_info", {})
+            run2 = data2.get("run_info", {})
+
+            def format_duration(info):
+                return f"{info.get('days', 0)}d {info.get('hours', 0)}h {info.get('minutes', 0)}m {info.get('seconds', 0)}s"
+
+            result += f"  File 1: {format_duration(run1)}\n"
+            result += f"  File 2: {format_duration(run2)}\n\n"
+
+            # Compare measurements (timerlat or cyclictest)
+            meas1 = data1.get("measurements", {})
+            meas2 = data2.get("measurements", {})
+
+            # Determine which measurement type to compare
+            measurement_type = None
+            if "timerlat" in meas1 and "timerlat" in meas2:
+                measurement_type = "timerlat"
+            elif "cyclictest" in meas1 and "cyclictest" in meas2:
+                measurement_type = "cyclictest"
+
+            if measurement_type:
+                result += f"{measurement_type.capitalize()} Statistics Comparison:\n"
+                result += "-" * 60 + "\n"
+
+                stats1 = meas1.get(measurement_type, {})
+                stats2 = meas2.get(measurement_type, {})
+
+                # Key metrics to compare
+                for metric in ["samples", "minimum", "maximum", "mean", "median", "standard_deviation"]:
+                    if metric in stats1 and metric in stats2:
+                        val1_data = stats1[metric]
+                        val2_data = stats2[metric]
+
+                        val1 = val1_data["value"]
+                        val2 = val2_data["value"]
+                        unit = val1_data.get("unit", "")
+
+                        result += f"  {metric}:\n"
+                        result += f"    File 1: {val1} {unit}\n"
+                        result += f"    File 2: {val2} {unit}\n"
+
+                        # Calculate difference for numeric values
+                        try:
+                            num1 = float(val1)
+                            num2 = float(val2)
+                            diff = num2 - num1
+                            if diff > 0:
+                                result += f"    Difference: +{diff:.2f} {unit} (File 2 is higher)\n"
+                            elif diff < 0:
+                                result += f"    Difference: {diff:.2f} {unit} (File 2 is lower)\n"
+                            else:
+                                result += f"    Difference: No change\n"
+
+                            # Calculate percentage change for non-zero values
+                            if num1 != 0 and metric != "samples":
+                                pct_change = ((num2 - num1) / num1) * 100
+                                result += f"    Percent change: {pct_change:+.2f}%\n"
+
+                        except (ValueError, TypeError):
+                            pass
+
+                        result += "\n"
+            else:
+                result += "Measurement Comparison: Different measurement types used\n"
+                result += f"  File 1: {', '.join(meas1.keys())}\n"
+                result += f"  File 2: {', '.join(meas2.keys())}\n\n"
+
+            return [TextContent(type="text", text=result)]
+
+        except ET.ParseError as e:
+            return [TextContent(
+                type="text",
+                text=f"Error parsing XML file: {str(e)}"
+            )]
+        except Exception as e:
+            return [TextContent(
+                type="text",
+                text=f"Error comparing results: {str(e)}"
+            )]
 
     else:
         return [TextContent(
-- 
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.