At some point, every software engineer will find themselves in a situation where they need to benchmark system performance and test the limits of what a given system can handle. This is a common problem in software engineering, and even more so in the applications that are well suited for Elixir.
Finding bottlenecks early on in an application can save a lot of time, money, and effort in the long run, and give developers confidence in the upper limit of a system.
In this post, we will introduce a tool called Benchee
to benchmark parts of an Elixir application. We will also show you how to integrate Benchee with your automated test suite.
By the end of the article, you'll understand Benchee's full functionality and capabilities, and will be able to use it to measure your application's performance.
Let's get going!
Why Do I Need to Benchmark My Elixir Application?
Benchmarking, in a nutshell, is the process of measuring the performance of a system under specific loads or conditions. As part of the benchmarking process, you will be able to identify potential bottlenecks in your system and areas to improve.
For example, we can use benchmarking tools to answer questions like:
- Can a system handle ten times the load of normal traffic?
- Can the system run on a smaller infrastructure to handle the same load?
- How long does it take to process 10, 100, 1,000, or 10,000 requests? Does the processing time scale linearly with the number of requests?
Just answering these questions alone can help your team avoid costly mistakes and proactively identify areas for improvement, avoiding downtime and unhappy users.
Benchmarking doesn't have to be expensive or time-consuming; it can be simple to get the right tools in place and make them part of an application's natural development life cycle.
What Is Benchee?
This is where Benchee comes in. Benchee is a tool that you can use to benchmark parts of an Elixir application. It is versatile and extensible, with more than a few plugins to enhance its functionality.
Prerequisites
Elixir Environment
To follow along, you will need to locally install Elixir and Phoenix. The easiest way to do so is to follow the official Elixir instructions, which will give you a couple of options for:
- Local installation on Linux, Windows, and macOS
- Dockerized versions of Elixir
- Package manager version setups
I recommend a local installation for the best results.
Setting Up Our Elixir Application
For this article's purposes, we will set up a simple Elixir application that can calculate the Fibonacci sequence.
Start by creating a new application with mix new fibonacci_benchmarking
:
As output, you will see the following:
Next, in your favorite editor, add the following code to the lib/fibonacci_benchmarking.ex
file:
Note: The original code can be found in rosettacode.org.
Go to the fibonacci_benchmarking
directory and run the following commands:
And once inside the elixir shell, you can run this:
If you see the above output, you have successfully set up your application, and are ready to proceed with Benchee.
Implement Benchmarking on an Elixir Application
First, we will need to install Benchee. Start by adding the following to your mix.exs
file:
Next, run the following command:
We can validate that Benchee is installed by running the Elixir shell and the following snippet:
Benchee might take a second or two to warm up, but on completion, you should see the following output:
The code above makes a call to FibonacciBenchmarking.list(10)
, and Benchee measures the time it takes to execute the function.
Let's take a moment to understand the output of Benchee. By default, Benchee will output the following information:
- ips stands for iterations per second. This number represents how many times a given function can be executed in a second. Higher is better.
- average is the average time it takes to execute the function. Lower is better.
- deviation is the standard deviation of the results. This is a measure of how much the results deviate from the average.
- median is the middle value of the results.
- 99th % - 99% of all the measured values are less than this value.
While running Benchee in this fashion can be useful for ad-hoc benchmarks, a much better method is to include Benchee as part of our unit tests.
Automate Benchee for Elixir and Run Tests
By default, all Elixir and Phoenix applications have a test
directory and use ExUnit
to run tests. Our goal is to get Benchee running as part of our test suite and test a different implementation of the Fibonacci sequence.
Start by creating a new file called test/benchee_unit_test.exs
, and copy the following code into it:
Go ahead and run mix test
on the console. Validate that the output looks like the following:
So far, we have integrated Benchee into our test suite and added the first test to validate one of the test cases. Let's add the second test case to compare. Update the test
function to:
Just like we did before, we can run the test suite with mix test
, and validate that the output looks like the following:
Our second test scenario tries to compare the performance of the Fibonacci sequence with a list of 1,000 numbers; however, this is not a very practical way to test with multiple inputs. We can take advantage of the Benchee.run
hooks and provide a list of inputs for each scenario.
Go ahead and open the test/benchee_unit_test.exs
file and replace the contents with this code:
In this new version of the code, we have generalized our generate list case to accept a list of inputs, and we can now run the test suite with mix test
.
However, because of the size of our last input, you will get a message like this:
As it happens, we hit a timeout error after 60 seconds. Fortunately, as part of the stack trace, we get a couple of suggestions on how to solve this problem. For now, update the test suite with this code:
Note: Depending on your system, running that last scenario will take a while; feel free to remove it to continue with the tutorial.
Now that we have a baseline of our Fibonacci sequence generator's performance, a common and useful exercise is to compare the performance of different implementations of the same algorithm. In this case, we have an alternative implementation of the Fibonacci sequence generator based on a recursive function.
Start by updating the lib/fibonacci_benchmarking.ex
file with the following code:
Following that, we will update the test/benchee_unit_test.exs
file to account for both implementations:
The update test case will run two scenarios side by side for each of the prescribed inputs, letting us compare their overall performance. Go ahead and run mix test
to see the results.
As you can see, our list function's Enum implementation is much slower than the Stream
implementation, especially when the input size is larger. Comparing the performance of the two implementations is valuable in understanding the trade-offs and will help you develop more performant applications.
When adding benchmarking tests to parts of your automated testing, consider the potential drawbacks, such as the increased time it takes to run the tests. In this case, the benchmarking tests are tagged with :benchmark
and can be excluded from the default test suite. This allows us to run the benchmarking tests separately from the unit tests and only when we need to.
A much better approach is to take advantage of CI/CD pipeline integration like GitHub Actions and run the benchmarking tests as part of the pull request validation process. This way, we can run the benchmarking tests as part of the CI/CD pipeline and get the results without having to run the tests locally.
Improving Benchee Reporting
Now, while seeing results on the console can be useful for a quick glance, the console is not the most convenient way to share results with your team. Benchee provides a number of different ways to export your results to a file.
For this example, we will use benchee_html
to generate an HTML report with our benchmarking test results. To do this, we will add the benchee_html
dependency to our mix.exs
file:
Next, we will update the test/benchee_unit_test.exs
file to generate the HTML report:
Let's go ahead and run the tests again:
On completion, you should see the following report open in your browser:
The HTML report provides a much more detailed view of the benchmarking results and allows us to share results with our team easily. For example:
In addition to the HTML report, Benchee also supports exporting results to JSON, CSV, and XML formats. Exporting results to a file is a great way to integrate them with automation, such as CI/CD pipelines.
Monitoring Your Elixir App in Production
Benchee can help you discover potential performance bottlenecks, but what about how fast things really are in your production app?
To be able to discover new and existing bottlenecks, and solve bugs and other issues your users may face, you need to use an APM. AppSignal has been supporting Elixir developers for years and seamlessly integrates with your app. Bonus: We're the only APM that ships stroopwafels to new users 😎
Wrapping Up and Next Steps
In this tutorial, we discovered how to benchmark Elixir applications with the Benchee library.
We also learned how to compare the performance of different implementations of the same algorithm.
Yet we have only scratched the surface of Benchee's capabilities. As a next step, I highly encourage you to explore the available Benchee configuration options and visualization plugins.
Happy coding!
P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!