ExkPasswd Performance Benchmarks
Mix.install([
{:exk_passwd, "~> 0.1.0"},
{:benchee, "~> 1.3"}
])
Introduction
This notebook provides comprehensive performance benchmarks for ExkPasswd.
Key Performance Features:
- O(1) dictionary lookups - Compile-time indexed word lists
- Batch optimization - Buffered random bytes for bulk generation
- Zero allocations - Efficient string building
- Pure Elixir - No external dependencies
Run these benchmarks on your hardware to see real-world performance!
Basic Generation Performance
Benchmark the main generate/0 and generate/1 functions:
Benchee.run(
%{
"generate()" => fn -> ExkPasswd.generate() end,
"generate(:default)" => fn -> ExkPasswd.generate(:default) end,
"generate(:xkcd)" => fn -> ExkPasswd.generate(:xkcd) end,
"generate(:web32)" => fn -> ExkPasswd.generate(:web32) end,
"generate(:wifi)" => fn -> ExkPasswd.generate(:wifi) end,
"generate(:security)" => fn -> ExkPasswd.generate(:security) end
},
time: 3,
memory_time: 1,
warmup: 1
)
Dictionary Operations
Test the compile-time indexed dictionary performance (should be O(1)):
alias ExkPasswd.Dictionary
Benchee.run(
%{
"Dictionary.size()" => fn -> Dictionary.size() end,
"Dictionary.min_length()" => fn -> Dictionary.min_length() end,
"Dictionary.max_length()" => fn -> Dictionary.max_length() end,
"Dictionary.all()" => fn -> Dictionary.all() end,
"Dictionary.words_of_length(5)" => fn -> Dictionary.words_of_length(5) end,
"Dictionary.words_of_length(8)" => fn -> Dictionary.words_of_length(8) end,
"Dictionary.words_between(4, 8)" => fn -> Dictionary.words_between(4, 8) end,
"Dictionary.random_word(5)" => fn -> Dictionary.random_word(5) end,
"Dictionary.random_word(8)" => fn -> Dictionary.random_word(8) end,
"Dictionary.random_word_between(4, 8)" => fn ->
Dictionary.random_word_between(4, 8)
end
},
time: 2,
memory_time: 1,
warmup: 1
)
Batch vs Individual Generation
Compare batch generation (with buffered random) to individual generation:
alias ExkPasswd.{Batch, Config}
settings = Config.preset(:default)
Benchee.run(
%{
"individual: 100 passwords" => fn ->
for _ <- 1..100, do: ExkPasswd.generate(settings)
end,
"batch: 100 passwords" => fn ->
Batch.generate_batch(100, settings)
end,
"individual: 1000 passwords" => fn ->
for _ <- 1..1000, do: ExkPasswd.generate(settings)
end,
"batch: 1000 passwords" => fn ->
Batch.generate_batch(1000, settings)
end,
"individual: 10000 passwords" => fn ->
for _ <- 1..10_000, do: ExkPasswd.generate(settings)
end,
"batch: 10000 passwords" => fn ->
Batch.generate_batch(10_000, settings)
end
},
time: 3,
memory_time: 2,
warmup: 1
)
Case Transform Performance
Test different case transformations:
alias ExkPasswd.Config
case_configs = %{
"lower" => Config.new!(num_words: 4, case_transform: :lower),
"upper" => Config.new!(num_words: 4, case_transform: :upper),
"capitalize" => Config.new!(num_words: 4, case_transform: :capitalize),
"alternate" => Config.new!(num_words: 4, case_transform: :alternate),
"random" => Config.new!(num_words: 4, case_transform: :random),
"invert" => Config.new!(num_words: 4, case_transform: :invert)
}
scenarios =
for {name, config} <- case_configs, into: %{} do
{"transform: #{name}", fn -> ExkPasswd.generate(config) end}
end
Benchee.run(scenarios, time: 2, memory_time: 1, warmup: 1)
Word Count Impact
How does the number of words affect performance?
word_count_scenarios =
for count <- 2..6, into: %{} do
config = Config.new!(num_words: count)
{"#{count} words", fn -> ExkPasswd.generate(config) end}
end
Benchee.run(word_count_scenarios, time: 2, memory_time: 1, warmup: 1)
Padding Performance
Test the impact of different padding configurations:
padding_scenarios = %{
"no padding" => fn ->
ExkPasswd.generate(Config.new!(num_words: 3, digits: {0, 0}, padding: %{before: 0, after: 0}))
end,
"2 digits" => fn ->
ExkPasswd.generate(Config.new!(num_words: 3, digits: {2, 0}))
end,
"2+2 digits" => fn ->
ExkPasswd.generate(Config.new!(num_words: 3, digits: {2, 2}))
end,
"digits + symbols" => fn ->
ExkPasswd.generate(
Config.new!(
num_words: 3,
digits: {2, 2},
padding: %{char: "!@#$%^&*", before: 1, after: 1, to_length: 0}
)
)
end
}
Benchee.run(padding_scenarios, time: 2, memory_time: 1, warmup: 1)
Strength Analysis Performance
How fast is password strength analysis?
# Pre-generate passwords
test_configs = [
ExkPasswd.Config.Presets.get(:xkcd),
ExkPasswd.Config.Presets.get(:web32),
ExkPasswd.Config.Presets.get(:wifi),
ExkPasswd.Config.Presets.get(:security)
]
test_passwords = Enum.map(test_configs, &ExkPasswd.generate/1)
Benchee.run(
%{
"Strength.analyze(xkcd)" => fn ->
ExkPasswd.Strength.analyze(Enum.at(test_passwords, 0), Enum.at(test_configs, 0))
end,
"Strength.analyze(web32)" => fn ->
ExkPasswd.Strength.analyze(Enum.at(test_passwords, 1), Enum.at(test_configs, 1))
end,
"Strength.analyze(wifi)" => fn ->
ExkPasswd.Strength.analyze(Enum.at(test_passwords, 2), Enum.at(test_configs, 2))
end,
"Strength.analyze(security)" => fn ->
ExkPasswd.Strength.analyze(Enum.at(test_passwords, 3), Enum.at(test_configs, 3))
end,
"Entropy.calculate()" => fn ->
ExkPasswd.Entropy.calculate(Enum.at(test_passwords, 0), Enum.at(test_configs, 0))
end
},
time: 2,
memory_time: 1,
warmup: 1
)
Character Substitution Performance
Test the impact of character substitutions:
no_subst = Config.new!(num_words: 4, case_transform: :capitalize)
with_subst =
Config.new!(
num_words: 4,
case_transform: :capitalize,
meta: %{
transforms: [
%ExkPasswd.Transform.Substitution{
mode: :always,
map: %{"a" => "@", "e" => "3", "i" => "1", "o" => "0", "s" => "$"}
}
]
}
)
Benchee.run(
%{
"no substitutions" => fn -> ExkPasswd.generate(no_subst) end,
"with substitutions" => fn -> ExkPasswd.generate(with_subst) end
},
time: 2,
memory_time: 1,
warmup: 1
)
Memory Usage Comparison
Compare memory allocation across different operations:
Benchee.run(
%{
"generate() default" => fn -> ExkPasswd.generate() end,
"generate() security" => fn -> ExkPasswd.generate(:security) end,
"batch 100" => fn -> Batch.generate_batch(100, Config.preset(:default)) end
},
time: 2,
memory_time: 2,
warmup: 1
)
Performance Summary
Based on typical results (will vary by hardware):
Expected Performance:
- Single password generation: ~10-50 microseconds
- Dictionary lookups: <1 microsecond (O(1) compile-time index)
- Batch generation speedup: 1.5-3x faster for 1000+ passwords
- Memory per password: ~1-5 KB
- Strength analysis: ~5-20 microseconds
Key Insights
- Dictionary is blazing fast - O(1) compile-time indexing works!
- Batch generation scales - Significant speedup for bulk operations
- Minimal memory - Efficient string building keeps allocations low
- Consistent performance - No surprises across different configurations
Run Full Benchmark Suite
To run the complete benchmark suite from the command line:
# From the project root
mix bench.all
# Individual benchmarks
mix bench.password
mix bench.dict
mix bench.batch
These scripts are located in the bench/ directory and output to the console.
Performance Tips
For maximum performance when generating many passwords:
# ✓ GOOD: Use batch generation
passwords = Batch.generate_batch(10_000, :default)
# ✗ SLOWER: Generate individually
passwords = for _ <- 1..10_000, do: ExkPasswd.generate(:default)
Batch generation uses buffered random bytes, reducing the number of calls to :crypto.strong_rand_bytes/1 and improving throughput significantly.
Next Steps
- Security Analysis - Password strength and entropy
- Advanced Usage - Custom configurations
- Quick Start - Back to basics