Recommendation: Always prefer JFR or pprof over collapsed text. Both preserve event types, line numbers, and thread information that collapsed text loses.
The event type is stored in jdk.ActiveSetting records and mapped automatically (parse.go:536-551):
if typ == p.TypeMap.T_ACTIVE_SETTING { s := p.ActiveSetting if s.Name == "event" { execEventName = normalizeExecEvent(s.Value) // Dynamic event discovery for hardware counters }}
This allows ap-query to handle any hardware counter event recorded by async-profiler.
# From Go runtimecurl http://localhost:6060/debug/pprof/profile?seconds=30 > profile.pb.gz# From async-profiler (alternative to JFR)asprof -d 30 -o pprof -f profile.pb.gz <pid># From py-spypy-spy record --format pprof -o profile.pb.gz -- python app.py
pprof profiles contain multiple SampleType fields (cpu/nanoseconds, samples/count, etc.). ap-query maps these to event types automatically (pprof.go:42-68):
func classifyPprofSampleType(st *profile.ValueType) (eventType string, priority int) { typ := strings.ToLower(st.Type) unit := strings.ToLower(st.Unit) switch { case typ == "cpu" && unit == "nanoseconds": return "cpu", 2 case typ == "samples" && unit == "count": return "cpu", 1 case typ == "alloc_space" && unit == "bytes": return "alloc", 2 // ... more mappings }}
When multiple SampleTypes map to the same event, the highest-priority wins (e.g., alloc_space/bytes over alloc_objects/count).
pprof locations are leaf-first with inline expansion. ap-query reverses to root-first for collapsed format compatibility (pprof.go:182-226):
func resolvePprofStack(sample *profile.Sample) ([]string, []uint32) { // Count inlined frames total := 0 for _, loc := range sample.Location { if len(loc.Line) == 0 { total++ // unsymbolized } else { total += len(loc.Line) } } // Reverse to root-first order frames := make([]string, total) lines := make([]uint32, total) idx := total - 1 for _, loc := range sample.Location { for _, line := range loc.Line { frames[idx] = line.Function.Name lines[idx] = uint32(line.Line) idx-- } } return frames, lines}
pprof stores thread information in sample labels:
func extractPprofThread(sample *profile.Sample) string { for k, v := range sample.Label { if k == "thread" && len(v) > 0 { return v[0] } } for k, v := range sample.NumLabel { if k == "thread_id" && len(v) > 0 { return fmt.Sprintf("thread-%d", v[0]) } } return ""}
Not all pprof producers include thread labels. Go runtime profiles typically don’t have per-sample thread info.
If stdin contains non-printable bytes (control chars, high bytes), ap-query treats it as pprof:
# From pprof HTTP endpointcurl -s http://localhost:6060/debug/pprof/profile?seconds=30 | ap-query hot -# From compressed pprof filecat profile.pb.gz | ap-query hot -
Detection logic (parse.go:901-912):
func stdinLooksBinary(data []byte) bool { n := len(data) if n > 256 { n = 256 } for _, b := range data[:n] { if b > 0x7e || (b < 0x20 && b != '\n' && b != '\r' && b != '\t') { return true } } return false}
If stdin contains only printable ASCII and whitespace, it’s treated as collapsed text:
# From echoecho "main;processRequest;queryDB 42" | ap-query hot -# From filecat stacks.txt | ap-query hot -# From ap-query collapseap-query collapse profile.jfr | ap-query hot -
If binary detection triggers but pprof parsing fails, ap-query falls back to collapsed text if the data is valid UTF-8. This handles collapsed text with non-ASCII method names (e.g., café, 日本語).