# `Exgit.Profiler`
[🔗](https://github.com/ivarvong/exgit/blob/v0.1.0/lib/exgit/profiler.ex#L1)

Lightweight profiler that aggregates `:telemetry` span events
into a structured trace.

`profile/1` runs a function while capturing every exgit
telemetry event emitted by or beneath the call. The return
value is `{result, profile}` where `profile` is a structured
breakdown: per-event totals, call counts, peak memory (when
available), and the ordered list of events for detailed
drill-down.

Designed for three audiences:

  * **Agent developers** running `Exgit.Profiler.profile(fn ->
    my_agent_step() end)` to see where time goes in ONE call.
    No telemetry-handler plumbing required — attach, run,
    detach happens automatically.

  * **Operational monitoring** where you want per-request
    breakdowns matched to the operation that triggered them.
    Attach once via `attach/1`, read back via `read/1`, detach
    via `detach/1`.

  * **Test suites** asserting invariants like "this operation
    triggered at most N transport.fetch calls" or "cache bytes
    never exceeded 64 MiB during this workload."

The profiler attaches to the standard `[:exgit, *]` event tree,
so anything the library emits is captured. Zero cost when no
profile is active.

## Examples

    # One-shot profile of an agent-style session.
    {results, profile} =
      Exgit.Profiler.profile(fn ->
        {:ok, repo} = Exgit.clone(url, lazy: true)
        {:ok, repo} = Exgit.FS.prefetch(repo, "HEAD", blobs: true)
        Exgit.FS.grep(repo, "HEAD", "anthropic") |> Enum.to_list()
      end)

    profile.totals
    # => %{
    #   "transport.fetch"    => %{count: 2, us: 850_000},
    #   "pack.parse"         => %{count: 2, us:  95_000},
    #   "fs.grep"            => %{count: 1, us:  11_000},
    #   "object_store.get"   => %{count: 275, us: 25_000},
    #   ...
    # }

    profile.peak_cache_bytes
    # => 3_506_422

    profile.events
    # full event stream, ordered by start time, for drill-down

## Attaching manually

`profile/1` is a convenience that handles attach/detach for one
call. For server processes that want to accumulate a profile
across many calls, use `attach/1` + `read/1` + `detach/1`:

    {:ok, handle} = Exgit.Profiler.attach()

    # ... do work ...

    profile = Exgit.Profiler.read(handle)
    Exgit.Profiler.detach(handle)

Thread-safe: the profiler uses an ETS table and atomic counters,
so concurrent calls from multiple processes accumulate into the
same profile without locking.

# `event_record`

```elixir
@type event_record() :: %{
  event: String.t(),
  duration_us: non_neg_integer(),
  metadata: map(),
  started_at: integer()
}
```

# `handle`

```elixir
@type handle() :: %{id: String.t(), table: :ets.tid()}
```

# `t`

```elixir
@type t() :: %{
  events: [event_record()],
  totals: totals(),
  peak_cache_bytes: non_neg_integer() | :unknown,
  total_us: non_neg_integer()
}
```

# `totals`

```elixir
@type totals() :: %{
  required(String.t()) =&gt; %{count: non_neg_integer(), us: non_neg_integer()}
}
```

# `attach`

```elixir
@spec attach() :: {:ok, handle()}
```

Attach a profiler to the `[:exgit, *]` event tree and return
a `handle` for later `read/1` / `detach/1` calls.

Returns `{:ok, handle}` on success. The handle is a plain map
carrying the internal ETS table; treat it as opaque.

# `detach`

```elixir
@spec detach(handle()) :: :ok
```

Detach the profiler and free its ETS table.

# `profile`

```elixir
@spec profile((-&gt; result)) :: {result, t()} when result: var
```

Run `fun` with profiling enabled. Returns `{result, profile}`
where `result` is `fun`'s return value and `profile` is a
`t:t/0` map.

Attaches + detaches automatically; profiling is scoped to the
lifetime of the call.

# `read`

```elixir
@spec read(handle()) :: t()
```

Read the profile accumulated so far on `handle`.

Only events fired from the calling process are included. This
matters when multiple profilers (or async tests) are attached
concurrently: each profiler sees only its own caller's events.

## Caveat: child processes

Events fired in processes spawned by `fun` (e.g.
`Task.async_stream` workers from parallel `FS.grep`) will
NOT be captured — they fire in worker pids, not the caller.
For profiling parallel workloads, use `attach/0` + read from
each worker, or emit your own spanning event at the parent
level.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
