Benchmarking a Go and chi RESTful API
The amount of time and effort a developer dedicates towards writing a function depends on the details they choose to focus on: coding conventions, structure, programming style, etc. Suppose a group of developers is presented a high-level prompt to write the same function: given some input, return some output. For example, given a list of numbers, return a sorted list of numbers. The actual implementation of the function is left entirely to the discretion of the developer. A quick, mathematical way to evaluate each developer's implementation of this function, without any additional code, is by its time complexity . Particularly, knowing each implementation's Big-O complexity tells us how it might perform in the worst case scenario, commonly when the size of the input is very large. However, time complexity fails to account for the hardware the function is executed upon, and it does not provide any tangible, quantifiable metrics to base decisions on. Metrics such as operation speed and total execution time assign real numerical values to the performance of a function. By adding benchmarks , developers can leverage these metrics to better inform them on how to improve their code. The Go programming language has a benchmarking utility in its built-in, standard library package testing . To benchmark code in Go, define a function with a name prefixed with Benchmark (followed by a capitalized segment of text) and accepts an argument of struct type B , which contains methods and values for determining the number of iterations to run, running multiple benchmarks in parallel, timing execution times, etc. Example : Note : The structure of a benchmark is similar to the structure of a test. Replace Test with Benchmark and t *testing.T with b *testing.B . This benchmark runs the Sum function for b.N iterations. Here, the benchmark runs for one billion iterations, which allows the benchmark function to reliably time and record each execution. Once the benchmark is completed, the results of this benchmark and the CPU of the machine running this benchmark are outputted to the terminal. On average, each iteration ran 0.6199 ns. Below, I'm going to show you... Clone a copy of the Go and chi RESTful API from GitHub to you machine: This RESTful API specifies five endpoints for performing operations on posts: If you would like to learn how to build this RESTful API, then please visit this blog post . In the basic-tests branch, a simple unit test is already provided in the routes/posts_test.go file. Because benchmarks must be placed in _tests.go files, let's place the benchmarks for the posts sub-router in the routes/posts_test.go file. Run the following command to install the project's dependencies: Note : If you run into installation issues, then verify that the version of Go running on your machine is v1.16 . Run the following command to execute the unit tests within the routes/posts_test.go file: To get started, open the routes/posts_test.go file. Let's name the benchmark function BenchmarkGetPostsHandler : Combine the code snippets together: ( routes/posts_test.go ) Run the benchmark: Here, benchmarks run sequentially. The benchmark ran a total of 97220 iterations with each iteration running, on average, 11439 ns. This represents the average time it took for each handler.ServeHTTP function (and by extension, PostsResource{}.List ) call to complete. Each iteration involved the allocation of, on average, 33299 bytes of memory. Memory was allocated, on average, 8 times per iteration. The for loop forces the benchmark to execute the function handler.ServeHTTP sequentially, one after the other. By running the benchmark with b.RunParallel , the total iterations b.N is divided amongst the machine's available threads (distributed amongst multiple goroutines). Having these iterations run concurrently helps to benchmark code that's inherently concurrent, such as sending requests and receiving responses, and deals with mutexes and/or shared resources. To parallelize benchmarks, wrap the benchmark code in b.RunParallel and replace the for loop with for pb.Next() : ( routes/posts_test.go ) Run the benchmark again. Notice that the benchmark values are similar to when we ran the benchmark sequentially with a for loop. To increase the number of cores (and goroutines) to run this benchmark against, add the cpu flag: Increasing the number of cores increases the number of goroutines running the benchmark iterations, which results in better performance. Click here for a final version of the route handler unit test. Try writing benchmarks for the other route handlers.