mirror of
https://github.com/leanprover/lean4.git
synced 2026-03-17 18:34:06 +00:00
This PR adds tooling for profiling Lean programs with human-readable function names in Firefox Profiler: - **`script/lean_profile.sh`** — One-command pipeline: record with samply, symbolicate, demangle, and open in Firefox Profiler - **`script/profiler/lean_demangle.py`** — Faithful port of `Name.demangleAux` from `NameMangling.lean`, with a postprocessor that folds compiler suffixes into compact annotations (`[λ, arity↓]`, `spec at context[flags]`) - **`script/profiler/symbolicate_profile.py`** — Resolves raw addresses via samply's symbolication API - **`script/profiler/serve_profile.py`** — Serves demangled profiles to Firefox Profiler without re-symbolication - **`PROFILER_README.md`** — Documentation including a guide to reading demangled names ### Example output in Firefox Profiler | Raw C symbol | Demangled | |---|---| | `l_Lean_Meta_Sym_main` | `Lean.Meta.Sym.main` | | `l_Lean_Meta_foo___redArg___lam__0` | `Lean.Meta.foo [λ, arity↓]` | | `l_Lean_MVarId_withContext___at__...___spec__2___boxed` | `Lean.MVarId.withContext [boxed] spec at Lean.Meta.bar[λ, arity↓]` | Example: <img width="1145" height="570" alt="image" src="https://github.com/user-attachments/assets/8d23cc6a-1b89-4c60-9f4a-9f9f0f6e7697" /> 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
118 lines
3.5 KiB
Python
118 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Lean name demangler for samply / Firefox Profiler profiles.
|
|
|
|
Reads a profile JSON (plain or gzipped), demangles Lean function names
|
|
in the string table, and writes the result back.
|
|
|
|
Usage:
|
|
python lean_demangle_profile.py profile.json -o profile-demangled.json
|
|
python lean_demangle_profile.py profile.json.gz -o profile-demangled.json.gz
|
|
"""
|
|
|
|
import argparse
|
|
import gzip
|
|
import json
|
|
import sys
|
|
|
|
from lean_demangle import demangle_lean_name
|
|
|
|
|
|
def _demangle_string_array(string_array):
|
|
"""Demangle Lean names in a string array in-place. Returns count."""
|
|
count = 0
|
|
for i, s in enumerate(string_array):
|
|
if not isinstance(s, str):
|
|
continue
|
|
demangled = demangle_lean_name(s)
|
|
if demangled != s:
|
|
string_array[i] = demangled
|
|
count += 1
|
|
return count
|
|
|
|
|
|
def rewrite_profile(profile):
|
|
"""
|
|
Demangle Lean names in a Firefox Profiler profile dict (in-place).
|
|
|
|
Handles two profile formats:
|
|
- Newer: shared.stringArray (single shared string table)
|
|
- Older/samply: per-thread stringArray (each thread has its own)
|
|
"""
|
|
count = 0
|
|
|
|
# Shared string table (newer Firefox Profiler format)
|
|
shared = profile.get("shared")
|
|
if shared is not None:
|
|
sa = shared.get("stringArray")
|
|
if sa is not None:
|
|
count += _demangle_string_array(sa)
|
|
|
|
# Per-thread string tables (samply format)
|
|
for thread in profile.get("threads", []):
|
|
sa = thread.get("stringArray")
|
|
if sa is not None:
|
|
count += _demangle_string_array(sa)
|
|
|
|
return count
|
|
|
|
|
|
def process_profile_file(input_path, output_path):
|
|
"""Read a profile, demangle names, write it back."""
|
|
is_gzip = input_path.endswith('.gz')
|
|
|
|
if is_gzip:
|
|
with gzip.open(input_path, 'rt', encoding='utf-8') as f:
|
|
profile = json.load(f)
|
|
else:
|
|
with open(input_path, 'r', encoding='utf-8') as f:
|
|
profile = json.load(f)
|
|
|
|
count = rewrite_profile(profile)
|
|
|
|
out_gzip = output_path.endswith('.gz') if output_path else is_gzip
|
|
|
|
if output_path:
|
|
if out_gzip:
|
|
with gzip.open(output_path, 'wt', encoding='utf-8') as f:
|
|
json.dump(profile, f, ensure_ascii=False)
|
|
else:
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
json.dump(profile, f, ensure_ascii=False)
|
|
else:
|
|
json.dump(profile, sys.stdout, ensure_ascii=False)
|
|
sys.stdout.write('\n')
|
|
|
|
return count
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Demangle Lean names in samply/Firefox Profiler profiles")
|
|
parser.add_argument('input', help='Input profile (JSON or .json.gz)')
|
|
parser.add_argument('-o', '--output',
|
|
help='Output path (default: stdout for JSON, '
|
|
'or input with -demangled suffix)')
|
|
args = parser.parse_args()
|
|
|
|
output = args.output
|
|
if output is None and not sys.stdout.isatty():
|
|
output = None # write to stdout
|
|
elif output is None:
|
|
# Generate output filename
|
|
inp = args.input
|
|
if inp.endswith('.json.gz'):
|
|
output = inp[:-8] + '-demangled.json.gz'
|
|
elif inp.endswith('.json'):
|
|
output = inp[:-5] + '-demangled.json'
|
|
else:
|
|
output = inp + '-demangled'
|
|
|
|
count = process_profile_file(args.input, output)
|
|
if output:
|
|
print(f"Demangled {count} names, wrote {output}", file=sys.stderr)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|