We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ptmorris05/scalene-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Profile comparison and regression detection."""
from typing import Any
from .models import ProfileComparison, ProfileResult
class ProfileComparator:
"""Compare two profile results to identify improvements and regressions."""
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,
)
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