compare_profiles
Compare two Scalene profiles to identify performance improvements and regressions after code optimization. Returns runtime and memory changes with a summary.
Instructions
Compare two profiles to measure optimization impact.
Args: before_id: Profile ID from original code after_id: Profile ID from optimized code
Returns: {runtime_change_pct, memory_change_pct, improvements, regressions, summary_text}
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| before_id | Yes | ||
| after_id | Yes |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/scalene_mcp/server.py:406-427 (handler)The compare_profiles tool handler: takes before_id and after_id, validates they exist in recent_profiles, delegates to comparator.compare(), and returns the ProfileComparison as a dict.
async def compare_profiles( before_id: str, after_id: str, ) -> dict[str, Any]: """Compare two profiles to measure optimization impact. Args: before_id: Profile ID from original code after_id: Profile ID from optimized code Returns: {runtime_change_pct, memory_change_pct, improvements, regressions, summary_text} """ if before_id not in recent_profiles: raise ValueError(f"Profile not found: {before_id}") if after_id not in recent_profiles: raise ValueError(f"Profile not found: {after_id}") before = recent_profiles[before_id] after = recent_profiles[after_id] comparison = comparator.compare(before, after) return comparison.model_dump() - src/scalene_mcp/server.py:430-430 (registration)Registration of compare_profiles as an MCP tool via server.tool()
server.tool(compare_profiles) - src/scalene_mcp/comparator.py:11-93 (helper)ProfileComparator.compare() - the core comparison logic between two ProfileResult objects. Computes runtime, memory, and CPU change percentages, identifies improvements and regressions, and returns a ProfileComparison model.
def compare( self, before: ProfileResult, after: ProfileResult, ) -> ProfileComparison: """ Compare two profiles to identify performance changes. Args: before: Profile from before optimization after: Profile from after optimization Returns: ProfileComparison with detailed change analysis """ # Runtime comparison runtime_before = before.summary.elapsed_time_sec runtime_after = after.summary.elapsed_time_sec runtime_change_percent = ( ((runtime_after - runtime_before) / runtime_before * 100) if runtime_before > 0 else 0.0 ) runtime_improved = runtime_change_percent < 0 # Memory comparison memory_before = before.summary.max_memory_mb memory_after = after.summary.max_memory_mb memory_change_percent = ( ((memory_after - memory_before) / memory_before * 100) if memory_before > 0 else 0.0 ) memory_improved = memory_change_percent < 0 # CPU comparison cpu_before = before.summary.total_cpu_samples cpu_after = after.summary.total_cpu_samples cpu_change_percent = ( ((cpu_after - cpu_before) / cpu_before * 100) if cpu_before > 0 else 0.0 ) cpu_improved = cpu_change_percent < 0 # Collect improvements and regressions improvements = self._identify_improvements(before, after) regressions = self._identify_regressions(before, after) # Generate summary summary = self._generate_comparison_summary( runtime_improved, memory_improved, cpu_improved, runtime_change_percent, memory_change_percent, improvements, regressions, ) return ProfileComparison( before_id=before.profile_id or "unknown", after_id=after.profile_id or "unknown", runtime_before_sec=runtime_before, runtime_after_sec=runtime_after, runtime_change_percent=runtime_change_percent, runtime_improved=runtime_improved, memory_before_mb=memory_before, memory_after_mb=memory_after, memory_change_percent=memory_change_percent, memory_improved=memory_improved, cpu_before_samples=cpu_before, cpu_after_samples=cpu_after, cpu_change_percent=cpu_change_percent, cpu_improved=cpu_improved, improvements=improvements, regressions=regressions, overall_improved=( runtime_improved or memory_improved or cpu_improved ) and len(regressions) == 0, summary_text=summary, ) - src/scalene_mcp/models.py:189-220 (schema)ProfileComparison pydantic model - the output schema returned by compare_profiles. Contains before_id, after_id, runtime/memory/cpu metrics, improvements list, regressions list, overall_improved boolean, and summary_text.
class ProfileComparison(BaseModel): """Comparison between two profiles.""" before_id: str after_id: str # Runtime changes runtime_before_sec: float runtime_after_sec: float runtime_change_percent: float runtime_improved: bool # Memory changes memory_before_mb: float memory_after_mb: float memory_change_percent: float memory_improved: bool # CPU changes cpu_before_samples: int cpu_after_samples: int cpu_change_percent: float cpu_improved: bool # Detailed changes improvements: list[str] = Field(default_factory=list) regressions: list[str] = Field(default_factory=list) # Overall overall_improved: bool summary_text: str - src/scalene_mcp/comparator.py:95-358 (helper)Helper methods in ProfileComparator: _identify_improvements, _identify_regressions, _generate_comparison_summary - supporting logic used by compare()
def _identify_improvements( self, before: ProfileResult, after: ProfileResult ) -> list[str]: """ Identify specific improvements between profiles. Args: before: Profile from before optimization after: Profile from after optimization Returns: List of improvement descriptions """ improvements = [] # Runtime improvements runtime_change = ( (after.summary.elapsed_time_sec - before.summary.elapsed_time_sec) / before.summary.elapsed_time_sec * 100 if before.summary.elapsed_time_sec > 0 else 0 ) if runtime_change < -5: # At least 5% improvement improvements.append( f"Runtime improved by {abs(runtime_change):.1f}% " f"({before.summary.elapsed_time_sec:.2f}s → " f"{after.summary.elapsed_time_sec:.2f}s)" ) # Memory improvements memory_change = ( (after.summary.max_memory_mb - before.summary.max_memory_mb) / before.summary.max_memory_mb * 100 if before.summary.max_memory_mb > 0 else 0 ) if memory_change < -5: # At least 5% improvement improvements.append( f"Peak memory reduced by {abs(memory_change):.1f}% " f"({before.summary.max_memory_mb:.1f}MB → " f"{after.summary.max_memory_mb:.1f}MB)" ) # Leak improvements leaks_before = len(before.summary.detected_leaks) leaks_after = len(after.summary.detected_leaks) if leaks_after < leaks_before: improvements.append( f"Memory leaks reduced from {leaks_before} to {leaks_after}" ) # Allocation improvements alloc_change = ( ( after.summary.total_allocations_mb - before.summary.total_allocations_mb ) / before.summary.total_allocations_mb * 100 if before.summary.total_allocations_mb > 0 else 0 ) if alloc_change < -10: # At least 10% improvement improvements.append( f"Total allocations reduced by {abs(alloc_change):.1f}%" ) return improvements def _identify_regressions( self, before: ProfileResult, after: ProfileResult ) -> list[str]: """ Identify performance regressions between profiles. Args: before: Profile from before optimization after: Profile from after optimization Returns: List of regression descriptions """ regressions = [] # Runtime regressions runtime_change = ( (after.summary.elapsed_time_sec - before.summary.elapsed_time_sec) / before.summary.elapsed_time_sec * 100 if before.summary.elapsed_time_sec > 0 else 0 ) if runtime_change > 5: # At least 5% regression regressions.append( f"⚠️ Runtime increased by {runtime_change:.1f}% " f"({before.summary.elapsed_time_sec:.2f}s → " f"{after.summary.elapsed_time_sec:.2f}s)" ) # Memory regressions memory_change = ( (after.summary.max_memory_mb - before.summary.max_memory_mb) / before.summary.max_memory_mb * 100 if before.summary.max_memory_mb > 0 else 0 ) if memory_change > 5: # At least 5% regression regressions.append( f"⚠️ Peak memory increased by {memory_change:.1f}% " f"({before.summary.max_memory_mb:.1f}MB → " f"{after.summary.max_memory_mb:.1f}MB)" ) # Leak regressions leaks_before = len(before.summary.detected_leaks) leaks_after = len(after.summary.detected_leaks) if leaks_after > leaks_before: regressions.append( f"⚠️ Memory leaks increased from {leaks_before} to {leaks_after}" ) return regressions def _generate_comparison_summary( self, runtime_improved: bool, memory_improved: bool, cpu_improved: bool, runtime_change: float, memory_change: float, improvements: list[str], regressions: list[str], ) -> str: """ Generate human-readable comparison summary. Args: runtime_improved: Whether runtime improved memory_improved: Whether memory improved cpu_improved: Whether CPU usage improved runtime_change: Runtime change percentage memory_change: Memory change percentage improvements: List of improvements regressions: List of regressions Returns: Formatted markdown summary """ summary_parts = ["# Profile Comparison\n"] # Overall assessment if improvements and not regressions: summary_parts.append("✅ **Overall: Improved**\n\n") elif regressions and not improvements: summary_parts.append("⚠️ **Overall: Regressed**\n\n") elif improvements and regressions: summary_parts.append("⚡ **Overall: Mixed Results**\n\n") else: summary_parts.append("➡️ **Overall: No Significant Change**\n\n") # Key metrics summary_parts.append("## Key Metrics\n\n") runtime_emoji = "✅" if runtime_improved else "⚠️" if runtime_change > 5 else "➡️" summary_parts.append( f"{runtime_emoji} **Runtime**: {runtime_change:+.1f}%\n" ) memory_emoji = "✅" if memory_improved else "⚠️" if memory_change > 5 else "➡️" summary_parts.append( f"{memory_emoji} **Memory**: {memory_change:+.1f}%\n\n" ) # Improvements if improvements: summary_parts.append("## Improvements\n\n") for improvement in improvements: summary_parts.append(f"- {improvement}\n") summary_parts.append("\n") # Regressions if regressions: summary_parts.append("## Regressions\n\n") for regression in regressions: summary_parts.append(f"- {regression}\n") summary_parts.append("\n") # Recommendations if regressions: summary_parts.append("## Recommendations\n\n") summary_parts.append( "- Review changes that may have introduced regressions\n" ) if any("Runtime" in r for r in regressions): summary_parts.append( "- Profile CPU hotspots to identify performance bottlenecks\n" ) if any("memory" in r.lower() for r in regressions): summary_parts.append( "- Check for increased allocations or memory leaks\n" ) return "".join(summary_parts) def get_file_changes( self, before: ProfileResult, after: ProfileResult, min_change_percent: float = 5.0 ) -> dict[str, dict[str, Any]]: """ Get per-file performance changes. Args: before: Profile from before optimization after: Profile from after optimization min_change_percent: Minimum change percentage to report Returns: Dictionary mapping filenames to their change metrics """ changes: dict[str, dict[str, Any]] = {} # Get all files from both profiles all_files = set(before.files.keys()) | set(after.files.keys()) for filename in all_files: before_metrics = before.files.get(filename) after_metrics = after.files.get(filename) if before_metrics and after_metrics: # File exists in both profiles cpu_change = ( ( after_metrics.total_cpu_percent - before_metrics.total_cpu_percent ) / before_metrics.total_cpu_percent * 100 if before_metrics.total_cpu_percent > 0 else 0.0 ) if abs(cpu_change) >= min_change_percent: changes[filename] = { "cpu_before": before_metrics.total_cpu_percent, "cpu_after": after_metrics.total_cpu_percent, "cpu_change_percent": cpu_change, "improved": cpu_change < 0, } elif before_metrics and not after_metrics: # File removed (likely optimization) changes[filename] = { "status": "removed", "cpu_before": before_metrics.total_cpu_percent, } elif after_metrics and not before_metrics: # File added (potential regression) changes[filename] = { "status": "added", "cpu_after": after_metrics.total_cpu_percent, } return changes