package cachegrind import ( "bufio" "compress/gzip" "fmt" "io" "os" "strconv" "strings" ) // ParseFile opens a cachegrind file (plain text or gzip) and returns a Profile. func ParseFile(path string) (*Profile, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("open %s: %w", path, err) } defer f.Close() var r io.Reader = f if strings.HasSuffix(path, ".gz") { gr, gzErr := gzip.NewReader(f) if gzErr != nil { // fall back to plain text if _, seekErr := f.Seek(0, io.SeekStart); seekErr != nil { return nil, fmt.Errorf("gzip open %s: %w", path, gzErr) } } else { defer gr.Close() r = gr } } return Parse(r) } // Parse reads cachegrind format from r and returns a Profile. // All cost lines for a function are summed into inclusive costs (includes callees). // Two passes are used: the first builds the function table, the second resolves call edges. func Parse(r io.Reader) (*Profile, error) { scanner := bufio.NewScanner(r) scanner.Buffer(make([]byte, 4*1024*1024), 4*1024*1024) var lines []string for scanner.Scan() { lines = append(lines, scanner.Text()) } if err := scanner.Err(); err != nil { return nil, err } p := &Profile{ByName: make(map[string][]*Function)} // Pass 1: collect events count and build fnTable (alias → *Function). flTable := map[int]string{} fnTable := map[int]*Function{} nEvents := 0 currentFile1 := "" for _, line := range lines { switch { case strings.HasPrefix(line, "events:"): p.Events = strings.Fields(strings.TrimPrefix(line, "events:")) nEvents = len(p.Events) case strings.HasPrefix(line, "cmd:"): p.Cmd = strings.TrimSpace(strings.TrimPrefix(line, "cmd:")) case strings.HasPrefix(line, "fl="): alias, name := parseAlias(line[3:]) if name != "" { flTable[alias] = name } currentFile1 = flTable[alias] case strings.HasPrefix(line, "fn="): alias, name := parseAlias(line[3:]) if name != "" { fn := &Function{ Name: name, File: currentFile1, Costs: make([]int64, nEvents), } fnTable[alias] = fn p.Functions = append(p.Functions, fn) p.ByName[name] = append(p.ByName[name], fn) } } } // Pass 2: accumulate costs and resolve call edges (all functions now known). var currentFn *Function var pendingCallee *Function pendingCallCount := int64(0) inCallCtx := false for _, line := range lines { if line == "" || strings.HasPrefix(line, "#") { continue } switch { case strings.HasPrefix(line, "events:"), strings.HasPrefix(line, "cmd:"), strings.HasPrefix(line, "version:"), strings.HasPrefix(line, "creator:"), strings.HasPrefix(line, "part:"), strings.HasPrefix(line, "positions:"), strings.HasPrefix(line, "ob="), strings.HasPrefix(line, "totals:"), strings.HasPrefix(line, "summary:"): // already handled or irrelevant case strings.HasPrefix(line, "fl="): inCallCtx = false case strings.HasPrefix(line, "fn="): alias, _ := parseAlias(line[3:]) currentFn = fnTable[alias] inCallCtx = false case strings.HasPrefix(line, "cfl="): // called file alias — not needed case strings.HasPrefix(line, "cfn="): alias, _ := parseAlias(line[4:]) pendingCallee = fnTable[alias] case strings.HasPrefix(line, "calls="): pendingCallCount = 0 parts := strings.Fields(line[6:]) if len(parts) > 0 { pendingCallCount, _ = strconv.ParseInt(parts[0], 10, 64) } inCallCtx = true default: if len(line) == 0 || line[0] < '0' || line[0] > '9' { continue } costs, err := parseCostLine(line, nEvents) if err != nil { continue } if inCallCtx && currentFn != nil && pendingCallee != nil { call := &Call{ Caller: currentFn, Callee: pendingCallee, Count: pendingCallCount, Costs: costs, } currentFn.Calls = append(currentFn.Calls, call) pendingCallee.CalledBy = append(pendingCallee.CalledBy, call) pendingCallee = nil pendingCallCount = 0 inCallCtx = false } if currentFn != nil { addCosts(currentFn.Costs, costs) } } } return p, nil } // parseAlias parses "(N)" or "(N) name" from the value portion of fl=/fn=. func parseAlias(s string) (int, string) { if !strings.HasPrefix(s, "(") { return 0, "" } end := strings.Index(s, ")") if end < 0 { return 0, "" } alias, _ := strconv.Atoi(s[1:end]) name := "" if len(s) > end+1 { name = strings.TrimSpace(s[end+1:]) } return alias, name } // parseCostLine parses "linenum cost1 cost2 ..." and returns the cost slice. func parseCostLine(line string, nEvents int) ([]int64, error) { parts := strings.Fields(line) if len(parts) < 2 { return nil, fmt.Errorf("short cost line: %q", line) } costs := make([]int64, nEvents) for i := 0; i < nEvents && i+1 < len(parts); i++ { v, err := strconv.ParseInt(parts[i+1], 10, 64) if err != nil { return nil, err } costs[i] = v } return costs, nil } func addCosts(dst, src []int64) { for i := range src { if i < len(dst) { dst[i] += src[i] } } }