1. Overview
Sometimes we need to measure how much time a single method is taking on our code. In this tutorial, we’ll learn how to calculate the elapsed time of a method in Scala using different approaches.
2. Calculate the Elapsed Time of a Method
There are a few ways of doing this, so let’s go through each.
2.1. Using System Time
The most naive approach relies on using the Java API method System.currentTimeMillis():
scala> val before = System.currentTimeMillis; myMethod(1000000); val totalTime=System.currentTimeMillis-before
before: Long = 1694035209622
totalTime: Long = 803
The System.currentTimeMillis() method returns the current system time in milliseconds. So we can measure the time our method takes as the difference between the moment right before calling our method and the moment right after it completes.
2.2. Using System nanoTime
Unfortunately, there are a few small problems with using the System.currentTimeMillis() as we did in the previous section. This may not be a big issue for most usages, but if we want to be more pedantic, we should be aware of them.
Some of the problems with using System.currentTimeMillis() include:
- The granularity of the returned value depends on the underlying operating system
- Existence of irregular and unpredictable leap seconds
- Clock drifting and clock synchronization may add adjustments during your measurements
To better understand all the details, we can check the detailed explanation here.
The good part is that by using the System.nanoTime() method instead, these problems get solved:
scala> val before = System.nanoTime; myMethod(1000000); val totalTime=System.nanoTime-before
before: Long = 194628234729436
totalTime: Long = 526819884
scala> val totalTimeMillis = totalTime / 1000000
totalTimeMillis: Long = 526
In order to convert from nanoseconds to milliseconds, we can just divide the result by 1000000, as we did.
2.3. Using a Wrapper Function
The previous approach offers us an insight into the performance of a method, but it’s a bit cumbersome to use. We can improve it using Scala language features by creating a wrapper function.
scala> def time[T](block: => T): T = {
| val before = System.nanoTime
| val result = block
| val after = System.nanoTime
| println("Elapsed time: " + (after - before) / 1000000 + "ms")
| result
| }
We can now call the method using our wrapper:
scala> time(myMethod(1000000))
Elapsed time: 779ms
res0: Unit
This solution is more elegant than the previous one because it hides the timed logic in the wrapper function.
2.4. Micro Benchmarking Using JMH
The previous approaches provide some guidance on how long a method takes to run, but if we want to measure actual production code, we should not rely on such a naive approach.
Because we’re using JVM to run our code, and because the JVM does a ton of performance optimizations to our code, doing performance benchmarks using that approach won’t provide reliable results.
Fortunately, there’s a library for that called JMH. The tool handles things like JVM warm-up and code-optimization paths, making benchmarking as simple as possible. We can find more details about its usage in this guide.
We must take into account that measuring a method’s run time can become much more complicated very easily. For instance, in code where the method has multiple return points. More combinations mean it’s harder to measure. We may have to move measurements somewhere else, which may not always be easy.
3. Conclusion
In this article, we’ve learned a naive approach to measure our method’s run time and also briefly discussed a better way – using the micro benchmarking world of JMH.