This commit is contained in:
122
tools/perf/extract_top_symbols.py
Executable file
122
tools/perf/extract_top_symbols.py
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract top self-time symbols from a pprof flamegraph SVG.
|
||||
|
||||
Usage:
|
||||
python3 tools/perf/extract_top_symbols.py flamegraphs/<sha>/baseline.svg [N=15]
|
||||
|
||||
Reports the top-N functions by aggregate sample count, filtered to drop
|
||||
system / dependency frames so kreuzberg::* hotspots surface clearly.
|
||||
|
||||
The SVG is the standard pprof flamegraph output; each rectangle has a
|
||||
`<title>fn (samples, percent%)</title>` element. We aggregate per-function
|
||||
sample counts across all stack instances — that's an over-counting heuristic
|
||||
(a function called from many places shows up larger than its true self-time),
|
||||
but it's the right signal for "is this function on the hot path?"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
# Filter out stack frames that are dependencies, the runtime, or system noise.
|
||||
# The remaining frames are kreuzberg's own code (or close enough to be actionable).
|
||||
EXCLUDE_PREFIXES = (
|
||||
"__", # __mh_execute_header, __os_lock_..., __pthread_...
|
||||
"_", # _open$NOCANCEL, _os_cpu_..., _pthread_...
|
||||
"onnx::", # ORT internals
|
||||
"ort_sys::",
|
||||
"tokio::",
|
||||
"std::",
|
||||
"core::",
|
||||
"alloc::",
|
||||
"_$LT$std",
|
||||
"image::", # image crate
|
||||
"pdf_oxide::", # pdf_oxide internals
|
||||
"memchr::",
|
||||
"regex::",
|
||||
"serde::",
|
||||
"tracing::",
|
||||
"lopdf::",
|
||||
"pprof::",
|
||||
)
|
||||
|
||||
# Explicit numeric address frames (raw hex / decimal) — appear when symbols are stripped.
|
||||
ADDRESS_PATTERN = re.compile(r"^\d+$|^0x[0-9a-f]+$")
|
||||
|
||||
|
||||
def is_kreuzberg_or_application(symbol: str) -> bool:
|
||||
"""Return True if the symbol is application-level (likely kreuzberg crate)."""
|
||||
if not symbol:
|
||||
return False
|
||||
if ADDRESS_PATTERN.match(symbol):
|
||||
return False
|
||||
return not symbol.startswith(EXCLUDE_PREFIXES)
|
||||
|
||||
|
||||
def short(symbol: str, max_len: int = 110) -> str:
|
||||
"""Trim a long symbol name for display, abbreviating generic args."""
|
||||
s = re.sub(r"<[^>]*>", "<>", symbol)
|
||||
if len(s) > max_len:
|
||||
s = "..." + s[-(max_len - 3) :]
|
||||
return s
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print(f"usage: {sys.argv[0]} <flamegraph.svg> [top_n=15]", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
svg_path = Path(sys.argv[1])
|
||||
top_n = int(sys.argv[2]) if len(sys.argv) > 2 else 15
|
||||
|
||||
if not svg_path.is_file():
|
||||
print(f"error: {svg_path} is not a file", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
content = svg_path.read_text()
|
||||
pattern = re.compile(r"<title>([^<]+) \(([0-9,]+) samples, ([\d.]+)%\)</title>")
|
||||
matches = pattern.findall(content)
|
||||
|
||||
if not matches:
|
||||
print(f"warning: no <title> elements found in {svg_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
samples_per_fn: Counter[str] = Counter()
|
||||
for fn, samples, _pct in matches:
|
||||
samples_per_fn[fn] += int(samples.replace(",", ""))
|
||||
|
||||
total_samples = sum(samples_per_fn.values()) or 1
|
||||
|
||||
# Application-level symbols, ranked by aggregate samples.
|
||||
app_ranked = sorted(
|
||||
((fn, s) for fn, s in samples_per_fn.items() if is_kreuzberg_or_application(fn)),
|
||||
key=lambda x: -x[1],
|
||||
)
|
||||
|
||||
if not app_ranked:
|
||||
print(
|
||||
"warning: no application-level symbols matched filters — "
|
||||
"is the binary built with --profile profiling? a release-stripped "
|
||||
"build only contains system frames.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
# Fall back to showing top frames regardless.
|
||||
app_ranked = sorted(samples_per_fn.items(), key=lambda x: -x[1])
|
||||
|
||||
print(f"# Top {top_n} symbols by aggregate sample count from {svg_path.name}")
|
||||
print(f"# (out of {len(samples_per_fn)} unique frames; total samples {total_samples})")
|
||||
print()
|
||||
print(f"{'rank':>4} {'samples':>8} {'%total':>7} symbol")
|
||||
print("-" * 80)
|
||||
for rank, (fn, samples) in enumerate(app_ranked[:top_n], start=1):
|
||||
pct = 100.0 * samples / total_samples
|
||||
print(f"{rank:>4} {samples:>8} {pct:>6.2f}% {short(fn)}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user