package tools import ( "fmt" "os" "path/filepath" "sort" "strings" "forge.lclr.dev/AI/xdebug-mcp/internal/cache" "forge.lclr.dev/AI/xdebug-mcp/internal/cachegrind" ) // loadProfile returns a parsed Profile from cache or by parsing the file. func loadProfile(filePath string, c *cache.Cache) (*cachegrind.Profile, error) { abs, err := filepath.Abs(filePath) if err != nil { return nil, fmt.Errorf("resolve path %s: %w", filePath, err) } info, err := os.Stat(abs) if err != nil { return nil, fmt.Errorf("file not found: %s", abs) } if p, ok := c.Get(abs, info.ModTime()); ok { return p, nil } p, err := cachegrind.ParseFile(abs) if err != nil { return nil, err } c.Set(abs, p, info.ModTime()) return p, nil } // findFunctions returns functions matching name (exact first, then contains). // Returns a non-empty errMsg if nothing is found. func findFunctions(p *cachegrind.Profile, name string) ([]*cachegrind.Function, string) { if fns, ok := p.ByName[name]; ok { return fns, "" } var matches []*cachegrind.Function for _, fn := range p.Functions { if strings.Contains(fn.Name, name) { matches = append(matches, fn) } } if len(matches) == 0 { return nil, fmt.Sprintf("function %q not found in profile (no exact or contains match)", name) } return matches, "" } // sortedByTime returns a copy of fns sorted by Costs[0] descending. func sortedByTime(fns []*cachegrind.Function) []*cachegrind.Function { sorted := make([]*cachegrind.Function, len(fns)) copy(sorted, fns) sort.Slice(sorted, func(i, j int) bool { ci, cj := int64(0), int64(0) if len(sorted[i].Costs) > 0 { ci = sorted[i].Costs[0] } if len(sorted[j].Costs) > 0 { cj = sorted[j].Costs[0] } return ci > cj }) return sorted } // formatCosts formats a cost slice as "Event=value Event=value". func formatCosts(costs []int64, events []string) string { parts := make([]string, 0, len(events)) for i, ev := range events { if i < len(costs) { parts = append(parts, fmt.Sprintf("%s=%d", ev, costs[i])) } } return strings.Join(parts, " ") }