# Sondeo

[![PyPI Version](https://img.shields.io/pypi/v/sondeo)](https://pypi.org/project/sondeo/)
[![Python Versions](https://img.shields.io/pypi/pyversions/sondeo)](https://pypi.org/project/sondeo/)
[![License](https://img.shields.io/pypi/l/sondeo)](https://github.com/jhd3197/sondeo/blob/main/LICENSE)
[![GitHub Stars](https://img.shields.io/github/stars/jhd3197/sondeo?style=social)](https://github.com/jhd3197/sondeo)

---

**Lightweight Python code profiler and log reporter.** Inspect execution time, memory, call stacks, and bottlenecks — zero dependencies, stdlib only, one decorator away.

> *Sondeo* (Spanish: "probe", "survey") — probe your code, survey its performance, report what it finds.

## Quick Start

```bash
pip install sondeo
```

### Profile a function (2 lines)

```python
from sondeo import profile, report

@profile
def process_data():
    return sum(range(1_000_000))

process_data()
report()
```

```
============================================================
  SONDEO PROFILING REPORT
============================================================

  TIMING
  --------------------------------------------------------
  process_data                                  12.34ms (4 calls)

============================================================
```

### Track memory allocations

```python
from sondeo import memprof, report

@memprof
def allocate_heavy():
    matrix = [[0] * 1000 for _ in range(1000)]
    return matrix

allocate_heavy()
report()
```

```
============================================================
  SONDEO PROFILING REPORT
============================================================

  MEMORY
  --------------------------------------------------------
  allocate_heavy                     peak=38.21MB  alloc=7.63MB

============================================================
```

### Trace call trees

```python
from sondeo import trace

with trace() as t:
    result = build_pipeline()

t.print_tree()
```

```
build_pipeline (45.12ms) [pipeline.py:23]
  load_config (2.31ms) [config.py:8]
  connect_db (18.44ms) [db.py:15]
    authenticate (5.22ms) [auth.py:31]
    pool_connection (12.91ms) [db.py:42]
  run_query (24.01ms) [db.py:67]
```

### Time any block

```python
from sondeo import timer

with timer("data loading"):
    data = load_csv("users.csv")

with timer("processing"):
    result = transform(data)

with timer("export"):
    save_parquet(result, "output.parquet")
```

## Why Sondeo?

| Feature | Sondeo | cProfile | py-spy | scalene |
|---------|--------|----------|--------|---------|
| **Setup** | `@profile` — done | Manual `Profile()` + `pstats` | External process | Separate command |
| **Dependencies** | Zero (stdlib only) | stdlib | Rust binary | C extensions |
| **Memory profiling** | Built-in `@memprof` | No | No | Yes |
| **Call trees** | Built-in `trace()` | Manual parsing | Flamegraph only | No |
| **Overhead** | Minimal — opt-in per function | All-or-nothing | Sampling | Sampling |
| **Output** | Text, JSON, HTML | pstats files | SVG | HTML |
| **Composable** | Decorators + context managers | One mode | One mode | One mode |

## Core API

### Decorators

```python
from sondeo import profile, memprof

# Time profiling with cProfile under the hood
@profile
def fast_function():
    ...

# With options
@profile(sort_by="tottime", top_n=10, print_stats=True)
def detailed_function():
    ...

# Async functions are supported too
@profile
async def fetch_all():
    ...

# Memory profiling with tracemalloc
@memprof
def memory_heavy():
    ...

# With options
@memprof(top_n=5, trace_depth=5)
def deep_memory():
    ...
```

### Structured Logging

```python
from sondeo import log, profile, report

@log(level="debug")
@profile
def process_user(user_id: int) -> dict[str, int]:
    log("loading user", user_id, level="info")
    return {"id": user_id}

process_user(42)

# Timeline includes both profiling + log events
report()

# Filter timeline to specific modules/levels/patterns
report(format="json", include_modules=["myapp"], levels=["warn", "error"], pattern="db")
```

### Context Managers

```python
from sondeo import timer, trace

# Time a block
with timer("my operation") as t:
    do_work()
print(f"Took {t.elapsed_ms:.2f}ms")

# Async timing
async with timer("async operation") as t:
    await do_async_work()
print(f"Took {t.elapsed_ms:.2f}ms")

# Trace all calls in a block
with trace(filter_stdlib=True) as t:
    complex_operation()
t.print_tree(min_ms=0.5)  # Only show calls > 0.5ms
```

### Reporting

```python
from sondeo import report

# Print human-readable report
report()

# Get structured JSON
data = report(format="json")

# Report without clearing collected data
report(clear=False)
```

### Programmatic Access

```python
from sondeo.profiler.timer import get_results, clear_results
from sondeo.profiler.timer import get_async_task_stats, get_concurrency_hints
from sondeo.profiler.memory import get_results as get_memory_results

# Access raw results
for result in get_results():
    print(f"{result.name}: {result.elapsed_ms}ms ({result.calls} calls)")

for mem in get_memory_results():
    print(f"{mem.name}: peak={mem.peak_mb}MB")
    for alloc_line, size_mb in mem.top_allocations:
        print(f"  {alloc_line}")

# Async task slow-path tracking
for task in get_async_task_stats():
    print(task.task_name, task.slowest_ms)

# Heuristic deadlock/contention hints
for hint in get_concurrency_hints():
    print("hint:", hint)

# Clear when done
clear_results()
```

### Call Tree Serialization

```python
from sondeo import trace

with trace() as t:
    my_pipeline()

# Export as dict (for JSON, databases, dashboards)
tree_data = t.to_dict()

# {
#   "total_calls": 42,
#   "children": [
#     {"name": "my_pipeline", "elapsed_ms": 123.4, "self_ms": 5.2,
#      "children": [...]},
#   ]
# }
```

## CLI

```bash
# Profile an entire script
sondeo run app.py

# JSON output for piping
sondeo run app.py --report json

# Open Cacao dashboard
sondeo run app.py --report ui

# Sort by total time, show top 30 entries
sondeo run app.py --sort tottime --top 30

# Render a saved JSON report as markdown (CI-friendly)
sondeo report .sondeo/latest.json --format markdown

# Check version
sondeo --version
```

## Architecture

```
┌──────────────────────────────────────────────────────────┐
│                       YOUR CODE                          │
│                                                          │
│   @profile          @memprof          with trace()       │
│      │                 │                   │             │
└──────┼─────────────────┼───────────────────┼─────────────┘
       │                 │                   │
┌──────┴─────────────────┴───────────────────┴─────────────┐
│                    SONDEO CORE                           │
│                                                          │
│  ┌──────────┐  ┌──────────────┐  ┌────────────────┐     │
│  │  timer   │  │   memory     │  │    trace       │     │
│  │ cProfile │  │ tracemalloc  │  │  sys.settrace  │     │
│  │ perf_ctr │  │  snapshots   │  │  call trees    │     │
│  └────┬─────┘  └──────┬───────┘  └───────┬────────┘     │
│       │               │                  │              │
│       └───────────────┼──────────────────┘              │
│                       │                                  │
│              ┌────────┴────────┐                         │
│              │  Result Store   │                         │
│              │  TimingResult   │                         │
│              │  MemoryResult   │                         │
│              │  TraceResult    │                         │
│              └────────┬────────┘                         │
│                       │                                  │
└───────────────────────┼──────────────────────────────────┘
                        │
          ┌─────────────┼─────────────┐
          │             │             │
    ┌─────┴─────┐ ┌─────┴─────┐ ┌────┴─────┐
    │  Console  │ │   JSON    │ │   HTML   │
    │  report() │ │  export   │ │ (planned)│
    └───────────┘ └───────────┘ └──────────┘
```

## Project Structure

```
sondeo/
├── __init__.py              # Public API: profile, timer, memprof, trace, report
├── profiler/
│   ├── timer.py             # @profile + timer() — cProfile & perf_counter
│   ├── memory.py            # @memprof — tracemalloc snapshots & diffs
│   └── trace.py             # trace() — sys.settrace call tree capture
├── reporter/
│   └── console.py           # Text & JSON output
└── cli/
    └── __init__.py           # sondeo run <script>
```

## Optional Extras

```bash
pip install sondeo[rich]   # Colored tables, tree views, sparklines
pip install sondeo[html]   # Self-contained HTML reports with flamegraphs
pip install sondeo[ui]     # Interactive Cacao dashboard
pip install sondeo[all]    # Everything
```

## Roadmap

See [ROADMAP.md](ROADMAP.md) for the full vision. Highlights:

- **v0.2** — Structured logging with correlation IDs and timeline view
- **v0.3** — Rich reports + Cacao UI dashboard (terminal, browser, CI)
- **v0.4** — Async/threading support
- **v0.5** — Comparative profiling and regression detection
- **v0.6** — Live monitoring with `sondeo attach <pid>`
- **v1.0** — Stable release with documented performance guarantees

## Part of the Cacao Ecosystem

Sondeo is a standalone tool under [cacao-research](https://github.com/cacao-research). Install `sondeo[ui]` for an interactive [Cacao](https://github.com/jhd3197/Cacao)-powered dashboard, or look forward to a future `cacao-sondeo` plugin for live profiling panels embedded directly in your Cacao apps.

## License

MIT
