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:
- Latencia p99: < 1ms para distribución local
- Throughput: > 100k ticks/segundo por nodo
- Escalabilidad: lineal hasta 10k subscribers