← Files Build iOS AppsARCHIVED FILE

skills/ios-memgraph-leaks/scripts/summarize_memgraph_leaks.py

5.63 KB · Oct 3, 2026 · 06:38 UTC

↓ Download file

#!/usr/bin/env python3
"""Summarize leaks output from an Apple .memgraph file."""

from __future__ import annotations

import argparse
import re
import subprocess
import sys
from collections import Counter
from pathlib import Path


LEAK_RE = re.compile(r"^Leak:\s+(?P<address>0x[0-9a-fA-F]+)\s+size=(?P<size>\d+)\s+(?P<rest>.*)$")
TOTAL_RE = re.compile(r"Process\s+\S+:\s+(?P<count>\d+)\s+leaks?\s+for\s+(?P<bytes>\d+)\s+total leaked bytes")


def run_leaks(args: list[str]) -> subprocess.CompletedProcess[str]:
    return subprocess.run(["leaks", *args], text=True, capture_output=True, check=False)


def parse_leaks(output: str) -> tuple[str | None, list[dict[str, str]]]:
    total = None
    leaks: list[dict[str, str]] = []
    for line in output.splitlines():
        if total is None:
            match = TOTAL_RE.search(line)
            if match:
                total = f"{match.group('count')} leaks / {match.group('bytes')} bytes"
        match = LEAK_RE.match(line)
        if match:
            fields = match.groupdict()
            rest = fields.pop("rest")
            rest = re.sub(r"^zone:\s+\S+\s+", "", rest)
            parts = re.split(r"\s{2,}", rest.strip(), maxsplit=2)
            if len(parts) == 3:
                fields["type"], fields["language"], fields["image"] = parts
            elif len(parts) == 2:
                fields["type"], fields["image"] = parts
                fields["language"] = "<unknown>"
            else:
                fields["type"] = rest.strip() or "<unknown>"
                fields["language"] = "<unknown>"
                fields["image"] = "<unknown>"
            leaks.append(fields)
    return total, leaks


def trace_excerpt(memgraph: Path, address: str, max_lines: int) -> str:
    result = run_leaks([f"--traceTree={address}", str(memgraph)])
    text = result.stdout or result.stderr
    lines = [line.rstrip() for line in text.splitlines() if line.strip()]
    return "\n".join(lines[:max_lines])


def group_by_type_excerpt(memgraph: Path, max_lines: int) -> str:
    result = run_leaks(["--groupByType", str(memgraph)])
    text = result.stdout or result.stderr
    lines = [line.rstrip() for line in text.splitlines() if line.strip()]
    return "\n".join(lines[:max_lines])


def render(memgraph: Path, trace_limit: int, trace_lines: int, raw_output: str) -> str:
    total, leaks = parse_leaks(raw_output)
    by_type = Counter(leak["type"] for leak in leaks)
    by_image = Counter(leak["image"] for leak in leaks)

    lines: list[str] = []
    lines.append(f"# Leak Summary: {memgraph}")
    lines.append("")
    lines.append(f"- Total: {total or 'not found'}")
    lines.append(f"- Parsed leak entries: {len(leaks)}")
    lines.append("")

    if by_type:
        lines.append("## Top Types")
        for name, count in by_type.most_common(20):
            lines.append(f"- {count}x {name}")
        lines.append("")

    if by_image:
        lines.append("## Top Images")
        for name, count in by_image.most_common(20):
            lines.append(f"- {count}x {name}")
        lines.append("")

    if leaks:
        lines.append("## Leak Entries")
        for leak in leaks[:50]:
            lines.append(
                f"- {leak['address']} size={leak['size']} type={leak['type']} "
                f"image={leak['image']}"
            )
        if len(leaks) > 50:
            lines.append(f"- ... {len(leaks) - 50} more")
        lines.append("")

    if trace_limit > 0 and leaks:
        lines.append("## TraceTree Excerpts")
        for leak in leaks[:trace_limit]:
            lines.append(f"### {leak['address']} {leak['type']}")
            excerpt = trace_excerpt(memgraph, leak["address"], trace_lines)
            lines.append("~~~text")
            lines.append(excerpt or "<no trace output>")
            lines.append("~~~")
            lines.append("")

    if leaks:
        lines.append("## Grouped Leak Tree")
        lines.append("Use this when `traceTree` has no roots, which is common for unreachable retain cycles.")
        lines.append("~~~text")
        lines.append(group_by_type_excerpt(memgraph, trace_lines) or "<no grouped leak output>")
        lines.append("~~~")
        lines.append("")

    lines.append("## Raw Commands")
    lines.append("~~~bash")
    lines.append(f"leaks --list {memgraph}")
    if leaks:
        lines.append(f"leaks --groupByType {memgraph}")
    if leaks:
        lines.append(f"leaks --traceTree={leaks[0]['address']} {memgraph}")
    lines.append("~~~")
    lines.append("")

    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("memgraph", type=Path)
    parser.add_argument("--trace-limit", type=int, default=0, help="Number of leaks to trace with --traceTree")
    parser.add_argument("--trace-lines", type=int, default=80, help="Max lines per traceTree excerpt")
    parser.add_argument("--out", type=Path, help="Write markdown summary to this file")
    args = parser.parse_args()

    if not args.memgraph.exists():
        print(f"memgraph not found: {args.memgraph}", file=sys.stderr)
        return 2

    result = run_leaks(["--list", str(args.memgraph)])
    raw = result.stdout or result.stderr
    total, leaks = parse_leaks(raw)
    if result.returncode != 0 and total is None and not leaks:
        print(raw, file=sys.stderr, end="" if raw.endswith("\n") else "\n")
        return result.returncode or 1

    summary = render(args.memgraph, args.trace_limit, args.trace_lines, raw)
    if args.out:
        args.out.parent.mkdir(parents=True, exist_ok=True)
        args.out.write_text(summary)
        print(args.out)
    else:
        print(summary)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: 0488f210ba3cd281f15c9e3d0597d7e9089969885ba51331e7a31ead0fb8de1e