Powered by AppSignal & Oban Pro

Benchmark ExkPasswd on your system

notebooks/benchmarks.livemd

Benchmark ExkPasswd on your system

Mix.install([
  {:exk_passwd, "~> 0.2.0"},
  {:benchee, "~> 1.5"}
])

Microbenchmarks are local measurements, not API guarantees. Keep runtime, hardware, power mode, and benchmark configuration with any published result.

Single-password generation

configs = %{
  default: ExkPasswd.Config.Presets.get(:default),
  xkcd: ExkPasswd.Config.Presets.get(:xkcd),
  wifi: ExkPasswd.Config.Presets.get(:wifi)
}

Benchee.run(
  Map.new(configs, fn {name, config} ->
    {Atom.to_string(name), fn -> ExkPasswd.generate(config) end}
  end),
  warmup: 1,
  time: 2,
  memory_time: 1
)

Batch versus individual generation

config = ExkPasswd.Config.Presets.get(:default)

Benchee.run(
  %{
    "individual 1,000" => fn ->
      for _ <- 1..1_000, do: ExkPasswd.generate(config)
    end,
    "buffered batch 1,000" => fn ->
      ExkPasswd.Batch.generate_batch(1_000, config)
    end
  },
  warmup: 1,
  time: 3,
  memory_time: 1
)

Do not assume the batch path wins at every size. Buffer refill cost, config validation, scheduler behavior, and the crypto implementation can change the result. Read the generated report rather than relying on a fixed speedup claim.

The checked report from 2026-07-10 (Apple M4 Max, Elixir 1.20.2, OTP 29.0.3) measured 2.20 ms versus 2.41 ms at 100 outputs and 23.10 ms versus 25.66 ms at 1,000 outputs, favoring the buffered batch by about 9–10%. At 10,000 outputs the individual loop measured 256.28 ms versus 285.50 ms for the batch, so the batch was about 11% slower and allocated about 3% more memory in that scenario.

Dictionary selection

Benchee.run(
  %{
    "count 4..8" => fn -> ExkPasswd.Dictionary.count_between(4, 8) end,
    "select 4..8" => fn -> ExkPasswd.Dictionary.random_word_between(4, 8) end,
    "list outputs 4..8" => fn -> ExkPasswd.Dictionary.words_between(4, 8) end
  },
  warmup: 1,
  time: 2,
  memory_time: 1
)

Common EFF ranges use precomputed tuples. Uncommon and custom ranges may scan the small by-length index and assemble a candidate tuple, so “every dictionary operation is O(1)” would be inaccurate.

Entropy analysis cost

password = ExkPasswd.generate(config)

Benchee.run(
  %{
    "generate default" => fn -> ExkPasswd.generate(config) end,
    "seen entropy" => fn -> ExkPasswd.Entropy.calculate_seen(config) end,
    "full report" => fn -> ExkPasswd.Entropy.calculate(password, config) end
  },
  warmup: 1,
  time: 2,
  memory_time: 1
)

Seen-entropy analysis enumerates reachable word outputs so it can detect transform collisions. It is intentionally more work than generating one password.

Repository benchmark suite

From a checkout:

mix bench.password
mix bench.dict
mix bench.batch
mix bench.all

Use CI=true mix bench.all only as a short compile/run smoke test. Its sampling windows are too short for performance conclusions.