Benchmarking

Mide, no adivines. Herramientas y técnicas para benchmarking correcto.⏱️ 2.5 horas

Benchee: benchmarking en Elixir

# mix.exs
{:benchee, "~> 1.1", only: :dev}

# benchmark.exs
Benchee.run(%{
  "map" => fn -> Enum.map(1..1000, &(&1 * 2)) end,
  "comprehension" => fn -> for x <- 1..1000, do: x * 2 end,
  "Stream" => fn -> 1..1000 |> Stream.map(&(&1 * 2)) |> Enum.to_list() end
}, time: 10, memory_time: 2)

Con inputs variables

Benchee.run(
  %{
    "json encode" => fn data -> Jason.encode!(data) end,
    "binary encode" => fn data -> MarketProtocol.encode(data) end
  },
  inputs: %{
    "small" => generate_tick(),
    "medium" => generate_ticks(100),
    "large" => generate_ticks(10_000)
  },
  time: 5
)

Medición manual

# :timer.tc para microsegundos
{time_us, result} = :timer.tc(fn ->
  my_function()
end)

# System.monotonic_time para nanosegundos
start = System.monotonic_time(:nanosecond)
result = my_function()
elapsed = System.monotonic_time(:nanosecond) - start

# Para estadísticas
samples = for _ <- 1..1000 do
  {time, _} = :timer.tc(&my_function/0)
  time
end

avg = Enum.sum(samples) / length(samples)
sorted = Enum.sort(samples)
p50 = Enum.at(sorted, div(length(sorted), 2))
p99 = Enum.at(sorted, trunc(length(sorted) * 0.99))

Profiling con :fprof

# Función a perfilar
:fprof.apply(&MyModule.slow_function/1, [arg])
:fprof.profile()
:fprof.analyse([totals: true, sort: :own])

# Con :eprof para profiling más ligero
:eprof.start_profiling([self()])
my_function()
:eprof.stop_profiling()
:eprof.analyze(:total)

Tracing con :recon_trace

# mix.exs
{:recon, "~> 2.5"}

# Trace llamadas a función
:recon_trace.calls({MyModule, :function, :_}, 100)

# Con timestamps
:recon_trace.calls(
  {MyModule, :function, fn _ -> :return_trace end},
  100,
  [scope: :local]
)

Métricas de producción

defmodule Metrics do
  def measure(name, fun) do
    start = System.monotonic_time()
    result = fun.()
    stop = System.monotonic_time()
    duration = System.convert_time_unit(stop - start, :native, :microsecond)

    :telemetry.execute(
      [:my_app, name],
      %{duration: duration},
      %{}
    )

    result
  end
end

# Uso
Metrics.measure(:encode_tick, fn ->
  MarketProtocol.encode_tick(data)
end)

Histogramas de latencia

defmodule LatencyHistogram do
  use GenServer

  # Buckets en microsegundos
  @buckets [10, 50, 100, 250, 500, 1000, 5000, 10000, :infinity]

  def start_link(_), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)

  def record(name, duration_us) do
    GenServer.cast(__MODULE__, {:record, name, duration_us})
  end

  def report(name) do
    GenServer.call(__MODULE__, {:report, name})
  end

  @impl true
  def init(_) do
    {:ok, %{}}
  end

  @impl true
  def handle_cast({:record, name, duration}, state) do
    bucket = Enum.find(@buckets, &(duration <= &1))
    hist = Map.get(state, name, init_histogram())
    new_hist = Map.update(hist, bucket, 1, &(&1 + 1))
    {:noreply, Map.put(state, name, new_hist)}
  end

  @impl true
  def handle_call({:report, name}, _from, state) do
    {:reply, Map.get(state, name, init_histogram()), state}
  end

  defp init_histogram do
    Map.new(@buckets, &{&1, 0})
  end
end
Ejercicio 19.1 Benchmark del protocolo Básico

Crea un benchmark completo de tu protocolo binario vs JSON:

  • Encode/decode de diferentes tamaños de payload
  • Medir throughput (messages/second)
  • Comparar uso de memoria
Ejercicio 19.2 End-to-end latency Avanzado

Mide la latencia end-to-end de tu sistema:

  • Desde generación de tick hasta llegada a subscriber
  • Histograma de latencias (p50, p95, p99)
  • Identificar cuellos de botella con profiling

Conexión con el proyecto final

Estableceremos baselines y objetivos de rendimiento: