79 lines
2.2 KiB
Go
79 lines
2.2 KiB
Go
|
|
package tools
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/mark3labs/mcp-go/mcp"
|
||
|
|
"github.com/mark3labs/mcp-go/server"
|
||
|
|
|
||
|
|
"forge.lclr.dev/AI/xdebug-mcp/internal/cache"
|
||
|
|
"forge.lclr.dev/AI/xdebug-mcp/internal/cachegrind"
|
||
|
|
)
|
||
|
|
|
||
|
|
// AnalyzeTool returns the MCP tool definition for analyze_profile.
|
||
|
|
func AnalyzeTool() mcp.Tool {
|
||
|
|
return mcp.NewTool("analyze_profile",
|
||
|
|
mcp.WithDescription("Analyze an Xdebug cachegrind profiling file. Returns global stats and top N functions sorted by inclusive time cost."),
|
||
|
|
mcp.WithString("file_path",
|
||
|
|
mcp.Required(),
|
||
|
|
mcp.Description("Absolute or relative path to the cachegrind file (.gz or plain text)"),
|
||
|
|
),
|
||
|
|
mcp.WithNumber("top_n",
|
||
|
|
mcp.Description("Number of top functions to return (default: 20)"),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// AnalyzeHandler returns the MCP handler for analyze_profile.
|
||
|
|
func AnalyzeHandler(c *cache.Cache) server.ToolHandlerFunc {
|
||
|
|
return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
|
|
filePath := req.GetString("file_path", "")
|
||
|
|
if filePath == "" {
|
||
|
|
return mcp.NewToolResultError("file_path is required"), nil
|
||
|
|
}
|
||
|
|
topN := req.GetInt("top_n", 20)
|
||
|
|
if topN <= 0 {
|
||
|
|
topN = 20
|
||
|
|
}
|
||
|
|
p, err := loadProfile(filePath, c)
|
||
|
|
if err != nil {
|
||
|
|
return mcp.NewToolResultError(err.Error()), nil
|
||
|
|
}
|
||
|
|
return mcp.NewToolResultText(Analyze(p, topN)), nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Analyze formats the top-N analysis of p. Exported for testing.
|
||
|
|
func Analyze(p *cachegrind.Profile, topN int) string {
|
||
|
|
sorted := sortedByTime(p.Functions)
|
||
|
|
if topN > len(sorted) {
|
||
|
|
topN = len(sorted)
|
||
|
|
}
|
||
|
|
|
||
|
|
var sb strings.Builder
|
||
|
|
fmt.Fprintf(&sb, "Command: %s\n", p.Cmd)
|
||
|
|
fmt.Fprintf(&sb, "Events: %s\n", strings.Join(p.Events, ", "))
|
||
|
|
fmt.Fprintf(&sb, "Functions: %d total\n\n", len(p.Functions))
|
||
|
|
|
||
|
|
peak := int64(0)
|
||
|
|
if len(sorted) > 0 && len(sorted[0].Costs) > 0 {
|
||
|
|
peak = sorted[0].Costs[0]
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(p.Events) > 0 {
|
||
|
|
fmt.Fprintf(&sb, "Top %d functions by %s:\n", topN, p.Events[0])
|
||
|
|
}
|
||
|
|
for i, fn := range sorted[:topN] {
|
||
|
|
pct := ""
|
||
|
|
if peak > 0 && len(fn.Costs) > 0 {
|
||
|
|
pct = fmt.Sprintf(" (%.1f%% of peak)", float64(fn.Costs[0])/float64(peak)*100)
|
||
|
|
}
|
||
|
|
fmt.Fprintf(&sb, " %3d. %-60s %s%s\n", i+1, fn.Name, formatCosts(fn.Costs, p.Events), pct)
|
||
|
|
fmt.Fprintf(&sb, " %s\n", fn.File)
|
||
|
|
}
|
||
|
|
|
||
|
|
return sb.String()
|
||
|
|
}
|