# Auto-Search Reference

## Investigation Tools

```bash
# Capture full bin file:
PYTHONMALLOC=malloc $RUNNER -m memray run --native -o /tmp/memray-investigate.bin -m pytest <test_file> -k "TESTFILTER" -v

# Stats (ranked allocations at peak):
$RUNNER -m memray stats /tmp/memray-investigate.bin

# Stats (total allocation sizes + histogram):
$RUNNER -m memray stats /tmp/memray-investigate.bin

# Flamegraph (HTML):
$RUNNER -m memray flamegraph /tmp/memray-investigate.bin -o /tmp/flamegraph.html
```

Adapt the capture command for the project's specific setup (virtualenv, PYTHONPATH, working directory, etc.).

## Micro-Benchmark Template

> See `../shared/micro-benchmark.md` for the A/B pattern, runner commands, and keep criteria.

**Memory-specific runner**: Use memray instead of timeit:
```bash
PYTHONMALLOC=malloc $RUNNER -m memray run --native -o /tmp/micro_a.bin /tmp/micro_bench_<name>.py a
PYTHONMALLOC=malloc $RUNNER -m memray run --native -o /tmp/micro_b.bin /tmp/micro_bench_<name>.py b
$RUNNER -m memray stats /tmp/micro_a.bin
$RUNNER -m memray stats /tmp/micro_b.bin
```

**Memory keep threshold**: >10 MiB or >10%.

## Per-Stage Profiling Template

For pipelines with sequential stages, use this template to identify which stage is the dominant allocator. Copy and adapt — replace the stage calls with the actual pipeline functions.

```python
# /tmp/profile_stages.py
import tracemalloc
import sys

# --- Adapt these imports and calls to your pipeline ---
from mymodule import load_data, parse, validate, enrich, export

def profile_pipeline(input_path):
    tracemalloc.start()
    snap0 = tracemalloc.take_snapshot()

    raw = load_data(input_path)
    snap1 = tracemalloc.take_snapshot()

    parsed = parse(raw)
    snap2 = tracemalloc.take_snapshot()

    validated = validate(parsed)
    snap3 = tracemalloc.take_snapshot()

    enriched = enrich(validated)
    snap4 = tracemalloc.take_snapshot()

    result = export(enriched)
    snap5 = tracemalloc.take_snapshot()

    # --- Report ---
    stages = [
        ("load_data",  snap0, snap1),
        ("parse",      snap1, snap2),
        ("validate",   snap2, snap3),
        ("enrich",     snap3, snap4),
        ("export",     snap4, snap5),
    ]
    print(f"{'Stage':<20} {'Delta MB':>10} {'Cumul MB':>10}")
    print("-" * 42)
    cumul = 0
    for name, before, after in stages:
        delta = sum(s.size_diff for s in after.compare_to(before, "filename"))
        delta_mb = delta / 1024 / 1024
        cumul += delta_mb
        print(f"{name:<20} {delta_mb:>+10.1f} {cumul:>10.1f}")

    peak_mb = tracemalloc.get_traced_memory()[1] / 1024 / 1024
    print(f"\nPeak: {peak_mb:.1f} MB")

    # Drill into the top stage — uncomment and set the right snapshots
    # diff = snap3.compare_to(snap2, "lineno")
    # print("\nTop lines in dominant stage:")
    # for stat in diff[:10]:
    #     print(stat)

if __name__ == "__main__":
    profile_pipeline(sys.argv[1])
```

Run it:
```bash
$RUNNER /tmp/profile_stages.py <input_file_or_arg>
```

After identifying the dominant stage, uncomment the drill-down section and set the snapshot pair to that stage's before/after snapshots.

## Allocation Patterns

Common patterns across Python applications and how they affect peak memory:

### Multi-phase pipelines

Applications that process data in sequential phases (e.g., load → transform → output) accumulate persistent allocations. The peak is usually in the latest phase because earlier allocations persist.

To reduce peak: free earlier-phase results when no longer needed, or make earlier stages produce smaller intermediates. Be careful not to free cached resources needed for throughput.

### Inference buffers

ML framework temporaries (ONNX `run()`, PyTorch forward pass, etc.) are typically freed when the function returns. They only contribute to peak if peak happens DURING that call. Verify with a micro-benchmark before assuming they matter.

### Cross-test caching

Standalone test runs include first-time model/resource loading. Full-suite runs cache across tests. **Always compare like-for-like.**

## Multi-Repo Projects

If the project spans multiple repos, create `codeflash/<tag>` in each repo you modify. Commit, milestone, and discard in all affected repos together.

## Profiling Tools

### tracemalloc (stdlib, zero-install)

Primary alternative to memray. Use for quick allocation tracing, per-line attribution, and snapshot diffs. No install needed — always available.

```python
import tracemalloc

tracemalloc.start()
# ... run code under investigation ...
snapshot = tracemalloc.take_snapshot()
top = snapshot.statistics("lineno")
for stat in top[:10]:
    print(stat)
```

**Comparing two snapshots** (before/after a suspected leak or optimization):
```python
tracemalloc.start()
snapshot_before = tracemalloc.take_snapshot()

# ... run the code path ...

snapshot_after = tracemalloc.take_snapshot()
diff = snapshot_after.compare_to(snapshot_before, "lineno")
for stat in diff[:10]:
    print(stat)
```

**Filtering by filename** to focus on project code:
```python
from tracemalloc import Filter
snapshot = snapshot.filter_traces([Filter(True, "*/myproject/*")])
```

### gc module debugging

Use to detect uncollectable reference cycles and trace who holds references to leaked objects.

```python
import gc

gc.set_debug(gc.DEBUG_SAVEALL)
gc.collect()
# gc.garbage now contains uncollectable objects (cycles with __del__)
for obj in gc.garbage[:5]:
    print(type(obj), gc.get_referrers(obj))
```

- **`gc.get_referrers(obj)`** — trace who holds a reference to a specific object. Useful for finding why something isn't being freed
- **`gc.collect()`** — force a full collection during investigation. Call before checking `gc.garbage` to ensure all collectable cycles are cleared first
- **`gc.get_objects()`** — list all tracked objects; combine with type filtering to count instances of a suspect class
