[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