123 lines
4.0 KiB
Python
Executable File
123 lines
4.0 KiB
Python
Executable File
#!/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())
|