Two-pass parser resolves forward cfn= references; all cost lines (including call edges) are accumulated as inclusive costs per function. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
200 lines
4.9 KiB
Go
200 lines
4.9 KiB
Go
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="):
|
|
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]
|
|
}
|
|
}
|
|
}
|