This section describes executors in ScalaMeter in more detail.
Every executor is responsible for executing tests and obtaining the performance measurements such as running time or the memory footprint for those tests. Also, every executor is parametrized by three components, namely:
-
Warmer
– warms up the JVM for the test(s) -
Aggregator
– applied to a series of measurements to obtain the definitive result (e.g. a mean value, a minimum value, a median, etc.) -
Measurer
– performs the actual measurements
Local executor
The local executor executes performance tests in the same JVM from which ScalaMeter was invoked. This is usually faster and can be useful if tests are needed quickly to compare several alternatives\ only roughly. The downside is that different, seemingly unrelated classes loaded in the same JVM may have influence performance in unpredictable ways, mainly due to the way that the JIT compiler and dynamic optimizations work. This means that adding a new test to the test suite, or invoking the test from a different program may seemingly change running times.
To warm up the JVM, warmups for all the tests are run first. This decreases the effect of changing the order of the tests on the performance. Then, for each test, its warmup is run again and the measurements are performed.
This executor gives results which are hardly reproducible and cannot be used for absolute performance regression testing.
Configuration:
exec.minWarmupRuns
– the minimum number of warmup runs to performexec.maxWarmupRuns
– the maximum number of warmup runs to performexec.warmupCovThreshold
– the CoV threshold for theWarmer.Default
stabilization detectionexec.benchRuns
– the desired number of measurements
Separate JVMs executor
This executor executes tests in a separate JVM.
Each JVM is first warmed up for the test, then the tests are executed.
This is the preferred way of executing performance tests, because the results
are reproducible.
Configuration:
- all the parameters seen in the local executor
exec.independentSamples
– the number of separate JVM instances between which the benchmark repetitions are divided
Measurers
A Measurer
is at the core of every executor, and it does all the work of running,
measuring the running times of your benchmarks,
analyzing them and possibly even changing the environment and inputs for them.
The executor simply tells the measurer how many measurements it wants,
and it’s the measurer’s job to provide
that many measurements, possibly by doing even more repetitions.
There are several predefined measurers.
Running time
Measurer.Default
simply does as many running time measurements as the user requests.
Measurer.IgnoringGC
does the same thing,
but ignores those measurements with a GC cycle.
If the number of measurements with a GC cycle
exceeds the number of desired measurements, it stops ignoring them.
This ensures termination.
Measurer.PeriodicInstantiation
is a mixin measurer,
which can be mixed in with the previous measurers.
It reinstantiates the value for the benchmark
every exec.reinstantiation.frequency
repetitions and will additionaly
perform a GC cycle if exec.reinstantiation.fullGC
is set to true
.
Measurer.OutlierElimination
is another mixin measurer,
which analyzes the measurements returned by other
measurers and possibly discards and repeats measurements.
It is described in more detail in the
regression testing section.
Measurer.AbsoluteNoise
is a mixin measurer which adds absolute noise to measurements.
Measurer.RelativeNoise
is a mixin measurer which adds relative noise to measurements.
The magnitude of the Gaussian noise is determined by the absolute
running time multiplied by the exec.noise.magnitude / 10
.
Adding noise can make a performance regression test less sensitive.
Memory footprint
ScalaMeter can measure memory footprint starting from version 0.4.
Measurer.MemoryFootprint
is a measurer the measures the memory footprint
of the object the return value of the benchmark snippet references.
It works by doing a GC and then recording
the initial memory occupancy of the JVM – call it membefore
.
It proceeds by instantiating the benchmark input from the generator,
executing the snippet and then letting go of the reference
to the input value so that it can be GCed.
The input value is thus not a part of the memory footprint.
Finally, it calls GC again and records the memory occupancy again –
call this value memafter
.
The value memafter - membefore
in kilobytes is the memory footprint it outputs.
The use is illustrated by the following snippet:
class MemoryTest extends Bench.Regression {
def persistor = new persistence.SerializationPersistor
override def measurer = new Executor.Measurer.MemoryFootprint
val sizes = Gen.range("size")(1000000, 5000000, 2000000)
performance of "MemoryFootprint" in {
performance of "Array" in {
using(sizes) config (
exec.independentSamples -> 6
) in { sz =>
(0 until sz).toArray
}
}
}
}
The verbose output is in this case very accurate –
a 32-bit integer array memory footprint should be its size multiplied by 4
:
...
[Full GC 20754K->1191K(2009792K), 0.0096360 secs]
[GC 105984K->20722K(2009792K), 0.0038210 secs]
[Full GC 20722K->20722K(2009792K), 0.0134250 secs]
[GC 20723K->20754K(2009792K), 0.0004800 secs]
[Full GC 20754K->1191K(2009792K), 0.0095290 secs]
[GC 105984K->20723K(2009792K), 0.0039180 secs]
[Full GC 20723K->20723K(2009792K), 0.0130130 secs]
Measurements: List(20000.016, 20000.016, 20000.016, 20000.016,
20000.016, 20000.016)
Obtained measurements:
size -> 1000000: 3927.424, 4000.016, 4000.016, 4000.016, ...
size -> 3000000: 11997.592, 12000.016, 12000.016, 12000.016, ...
size -> 5000000: 20000.016, 20000.016, 20000.016, 20000.016, ...
And the final output:
:::Summary of regression test results - OverlapIntervals():::
Test group: MemoryFootprint.Array
- MemoryFootprint.Array.Test-0 measurements:
- at size -> 1000000, 1 alternatives: passed
(ci = <3958.34 kB, 4017.49 kB>, significance = 1.0E-10)
- at size -> 3000000, 1 alternatives: passed
(ci = <11998.62 kB, 12000.60 kB>, significance = 1.0E-10)
- at size -> 5000000, 1 alternatives: passed
(ci = <20000.02 kB, 20000.02 kB>, significance = 1.0E-10)
Method Invocation Count
The invocation count measurers are used to count the total number of invocations of a specific method, or a group of methods.
There are two implementations of the InvocationCount
trait and both of them need instrumented classpath to work,
hence SeparateJvmsExecutor
must be used as an executor.
BoxingCount
simply counts (auto)boxing of primitives:
Boolean
, Byte
, Char
, Short
, Int
, Long
, Float
and Double
.
You can either specify primitives you would like to count or
count all of them by using the BoxingCount.all()
method.
class BoxingCountTest extends Bench.MicroBenchmark {
// we want to measure (auto)boxing of short, int and long values
override def measurer =
BoxingCount(classOf[Short], classOf[Int], classOf[Long])
// benchmark body
}
MethodInvocationCount
can count invocations of arbitrary methods.
It is configured with its InvocationCountMatcher
constructor argument.
class MethodInvocationCountTest extends Bench.OfflineReport {
override def measurer =
MethodInvocationCount(
InvocationCountMatcher.forName(
"scala.collection.immutable.Vector", "length"))
// benchmark body
}
InvocationCountMatcher
matches classes and their methods.
The methods are instrumented so that each time they are invoked,
they increase their corresponding counter.
It can separately match classes that should be instrumented,
and methods within those classes.
Class Matcher
A ClassMatcher
is a component that selects classes whose methods
should be later matched by a MethodMatcher
.
- The
ClassMatcher.ClassName
matches exactly one class specified by a name. - The
ClassMatcher.Regex
matches every class whose name matches the specified regex. - The
ClassMatcher.Descendants
matches any class that is a subclass of the specified class. This subclass relationship can be transitive, or only direct - this is specified with a parameter. In the inheritance hierarchy, both the interfaces and the superclasses are considered.
Method Matcher
After the classes are selected by the ClassMatcher
,
methods within those classes are filtered with the MethodMatcher
.
We support the following MethodMatcher
implementations:
- The
MethodMatcher.MethodName
matches methods with the specified name. - The
MethodMatcher.Allocation
matches allocations of a class. - The
MethodMatcher.Regex
matches every method whose name matches the given regular expression. - The
MethodMatcher.Full
matches both the method name and the method signature, where the signature is specified with the internal format that describes the method arguments and its return type.
You can find various examples of invocation count measurers in the scalameter-examples repo.
You can take a look at the ScalaMeter API or the source code to figure out how different measurers work in more detail.