Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign up
Find file
Copy path
scalameter/scalameter-core/src/main/scala/org/scalameter/Measurer.scala
Find file
Copy path
Fetching contributors…
| package org.scalameter | |
| import org.scalameter.execution.invocation.InvocationCountMatcher | |
| import org.scalameter.utils.withGCNotification | |
| import scala.collection._ | |
| import scala.compat._ | |
| import scala.runtime.BoxesRunTime | |
| import scala.util.matching.Regex | |
| trait Measurer[V] extends Serializable { self => | |
| def name: String | |
| def measure[T](context: Context, measurements: Int, setup: T => Any, | |
| tear: T => Any, regen: () => T, snippet: T => Any): Seq[Quantity[V]] | |
| def map[U](f: Quantity[V] => Quantity[U]) = new Measurer[U] { | |
| def name = self.name | |
| def measure[T](context: Context, measurements: Int, setup: T => Any, | |
| tear: T => Any, regen: () => T, snippet: T => Any): Seq[Quantity[U]] = | |
| self.measure(context, measurements, setup, tear, regen, snippet).map(f) | |
| override def usesInstrumentedClasspath: Boolean = self.usesInstrumentedClasspath | |
| override def prepareContext(context: Context): Context = | |
| self.prepareContext(context) | |
| override def beforeExecution(context: Context): Unit = self.beforeExecution(context) | |
| override def afterExecution(context: Context): Unit = self.afterExecution(context) | |
| } | |
| /** Indicates if a measurer uses instrumented classpath - | |
| * if `true` measurer must be run using an executor that spawns separate JVMs. | |
| */ | |
| def usesInstrumentedClasspath: Boolean = false | |
| /** Modifies the initial test context. | |
| * | |
| * This method is invoked before the `PerformanceTest` object's ctor is invoked. | |
| * The key-value pairs that the [[org.scalameter.Measurer]] adds to | |
| * the [[org.scalameter.Context]] in this method are visible to all | |
| * the test snippets within the `PerformanceTest` class. | |
| * | |
| * Most measurers do not need to add any specific keys, | |
| * so the default implementation just returns the `context`. | |
| */ | |
| def prepareContext(context: Context): Context = context | |
| /** Does some side effects before execution of all benchmarks in a performance test. | |
| * | |
| * This method is invoked in the `PerformanceTest` `executeTests` method | |
| * just before execution of any benchmarks. | |
| * | |
| * Most measurers do not need add additional context keys in [[prepareContext]], | |
| * so the default implementation just does nothing. | |
| */ | |
| def beforeExecution(context: Context): Unit = () | |
| /** Does some final cleanup after execution of all benchmarks in a performance test. | |
| * | |
| * This method is invoked in the `PerformanceTest` `executeTests` method | |
| * just after execution of all benchmarks. | |
| * | |
| * Most measurers do not need to do any side effects in [[beforeExecution]], | |
| * so the default implementation just does nothing. | |
| */ | |
| def afterExecution(context: Context): Unit = () | |
| } | |
| object Measurer { | |
| import Key._ | |
| /** Measurer that measures nothing. | |
| */ | |
| def None[V] = new Measurer[V] { | |
| def name = "None" | |
| def measure[T](context: Context, measurements: Int, setup: T => Any, | |
| tear: T => Any, regen: () => T, snippet: T => Any): Seq[Quantity[V]] = ??? | |
| } | |
| trait Timer extends Measurer[Double] | |
| /** Mixin for measurers whose benchmarked value is based on the current iteration. | |
| */ | |
| trait IterationBasedValue { | |
| /** Returns the value used for the benchmark at `iteration`. | |
| * May optionally call `regen` to obtain a new value for the benchmark. | |
| * | |
| * By default, the value `v` is always returned and the value for the | |
| * benchmark is never regenerated. | |
| */ | |
| protected def valueAt[T]( | |
| context: Context, iteration: Int, regen: () => T, v: T | |
| ): T = v | |
| } | |
| object Default { | |
| def apply() = new Default | |
| def withNanos() = new Default map { case Quantity(v, _) => | |
| Quantity(v * 1000000.0, "ns") | |
| } | |
| } | |
| /** A default measurer executes the test as many times as specified and returns the | |
| * sequence of measured times. | |
| */ | |
| class Default extends Timer with IterationBasedValue { | |
| @volatile private var snippetResult: Any = null | |
| def name = "Measurer.Default" | |
| def measure[T](context: Context, measurements: Int, setup: T => Any, | |
| tear: T => Any, regen: () => T, snippet: T => Any): Seq[Quantity[Double]] = { | |
| var iteration = 0 | |
| val times = mutable.ListBuffer.empty[Quantity[Double]] | |
| var value = regen() | |
| while (iteration < measurements) { | |
| value = valueAt(context, iteration, regen, value) | |
| setup(value) | |
| val start = System.nanoTime | |
| snippetResult = snippet(value) | |
| val end = System.nanoTime | |
| val time = Quantity((end - start) / 1000000.0, "ms") | |
| tear(value) | |
| times += time | |
| iteration += 1 | |
| } | |
| snippetResult = null | |
| log.verbose(s"measurements: ${times.mkString(", ")}") | |
| times.result() | |
| } | |
| } | |
| /** A measurer that discards measurements during which it detects GC cycles. | |
| * | |
| * Assume that `M` measurements are requested. | |
| * To prevent looping forever, after the number of measurement failed due to GC | |
| * exceeds the number of successful measurements by more than `M`, the subsequent | |
| * measurements are accepted regardless of whether GC cycles occur. | |
| */ | |
| class IgnoringGC extends Timer with IterationBasedValue { | |
| override def name = "Measurer.IgnoringGC" | |
| def measure[T](context: Context, measurements: Int, setup: T => Any, | |
| tear: T => Any, regen: () => T, snippet: T => Any): Seq[Quantity[Double]] = { | |
| val times = mutable.ListBuffer.empty[Quantity[Double]] | |
| var okcount = 0 | |
| var gccount = 0 | |
| var ignoring = true | |
| var value = regen() | |
| while (okcount < measurements) { | |
| value = valueAt(context, okcount + gccount, regen, value) | |
| setup(value) | |
| @volatile var gc = false | |
| val time = withGCNotification { n => | |
| dyn.currentContext.withValue(context) { | |
| gc = true | |
| log.verbose("GC detected.") | |
| } | |
| } { | |
| val start = System.nanoTime | |
| snippet(value) | |
| val end = System.nanoTime | |
| Quantity((end - start) / 1000000.0, "ms") | |
| } | |
| tear(value) | |
| if (ignoring && gc) { | |
| gccount += 1 | |
| if (gccount - okcount > measurements) ignoring = false | |
| } else { | |
| okcount += 1 | |
| times += time | |
| } | |
| } | |
| log.verbose(s"${if (ignoring) "All GC time ignored" | |
| else "Some GC time recorded"}, accepted: $okcount, ignored: $gccount") | |
| log.verbose(s"measurements: ${times.mkString(", ")}") | |
| times.result() | |
| } | |
| } | |
| /** A mixin measurer which causes the value for the benchmark to be reinstantiated | |
| * every `Key.exec.reinstantiation.frequency` measurements. | |
| * Before the new value has been instantiated, a full GC cycle is invoked if | |
| * `Key.exec.reinstantiation.fullGC` is `true`. | |
| */ | |
| trait PeriodicReinstantiation[V] extends Measurer[V] with IterationBasedValue { | |
| import exec.reinstantiation._ | |
| abstract override def name = s"${super.name}+PeriodicReinstantiation" | |
| def defaultFrequency = 10 | |
| def defaultFullGC = false | |
| protected override def valueAt[T]( | |
| context: Context, iteration: Int, regen: () => T, v: T | |
| ) = { | |
| val freq = context.goe(frequency, defaultFrequency) | |
| val fullgc = context.goe(fullGC, defaultFullGC) | |
| if ((iteration + 1) % freq == 0) { | |
| log.verbose("Reinstantiating benchmark value.") | |
| if (fullgc) Platform.collectGarbage() | |
| val nv = regen() | |
| nv | |
| } else v | |
| } | |
| } | |
| /** A mixin measurer which detects outliers (due to an undetected GC or JIT) and | |
| * requests additional measurements to replace them. | |
| * Outlier elimination can also eliminate some pretty bad allocation patterns in | |
| * some cases. Only outliers from above are considered. | |
| * | |
| * When detecting an outlier, up to `Key.exec.outliers.suspectPercent`% (with a | |
| * minimum of `1`) of worst times will be considered. | |
| * For example, given `Key.exec.outliers.suspectPercent = 25` the times: | |
| * | |
| * {{{ | |
| * 10, 11, 10, 12, 11, 11, 10, 11, 44 | |
| * }}} | |
| * | |
| * times `12` and `44` are considered for outlier elimination. | |
| * | |
| * Given the times: | |
| * | |
| * {{{ | |
| * 10, 12, 14, 55 | |
| * }}} | |
| * | |
| * the time `55` will be considered for outlier elimination. | |
| * | |
| * A potential outlier (suffix) is removed if removing it increases the coefficient | |
| * of variance by at least `Key.exec.outliers.covMultiplier` times. | |
| */ | |
| trait OutlierElimination[V] extends Measurer[V] { | |
| import exec.outliers._ | |
| implicit def numeric: Numeric[V] | |
| abstract override def name = s"${super.name}+OutlierElimination" | |
| def eliminateLow = false | |
| def covMultiplierModifier = 1.0 | |
| abstract override def measure[T](context: Context, measurements: Int, | |
| setup: T => Any, tear: T => Any, regen: () => T, snippet: T => Any | |
| ): Seq[Quantity[V]] = { | |
| import utils.Statistics._ | |
| import Numeric.Implicits._ | |
| implicit val ord = Ordering.by((q: Quantity[V]) => q.value) | |
| var results = | |
| super.measure(context, measurements, setup, tear, regen, snippet).sorted | |
| val suspectp = context(suspectPercent) | |
| val covmult = context(covMultiplier) | |
| val suspectnum = math.max(1, results.length * suspectp / 100) | |
| var retleft = context(retries) | |
| def suffixLength(rs: Seq[Double]): Int = { | |
| import utils.Statistics._ | |
| var minlen = 1 | |
| while (minlen <= suspectnum) { | |
| val cov = CoV(rs) | |
| val covinit = CoV(rs.dropRight(minlen)) | |
| val confirmed = | |
| if (covinit != 0.0) cov > covmult * covinit * covMultiplierModifier | |
| else mean(rs.takeRight(minlen)) > 1.2 * mean(rs.dropRight(minlen)) | |
| if (confirmed) return minlen | |
| else minlen += 1 | |
| } | |
| 0 | |
| } | |
| def outlierExists(rs: Seq[Double]) = { | |
| suffixLength(rs) > 0 || (eliminateLow && suffixLength(rs.reverse) > 0) | |
| } | |
| var best = results | |
| while (outlierExists(results.map(_.value.toDouble)) && retleft > 0) { | |
| val prefixlen = suffixLength(results.reverse.map(_.value.toDouble)) | |
| val suffixlen = suffixLength(results.map(_.value.toDouble)) | |
| val formatted = results.map(t => f"${t.value.toDouble}%.3f") | |
| log.verbose(s"Detected $suffixlen outlier(s): ${formatted.mkString(", ")}") | |
| results = { | |
| if (eliminateLow) ( | |
| super.measure(context, prefixlen, setup, tear, regen, snippet) ++ | |
| results.drop(prefixlen).dropRight(suffixlen) ++ | |
| super.measure(context, suffixlen, setup, tear, regen, snippet) | |
| ).sorted else (results.dropRight(suffixlen) ++ | |
| super.measure(context, suffixlen, setup, tear, regen, snippet)).sorted | |
| } | |
| if (CoV(results.map(_.value.toDouble)) < CoV(best.map(_.value.toDouble))) | |
| best = results | |
| retleft -= 1 | |
| } | |
| log.verbose("After outlier elimination: " + best.mkString(", ")) | |
| best | |
| } | |
| } | |
| /** A measurer which adds noise to the measurement. | |
| * | |
| * @define noise This measurer makes the regression tests more solid. While certain | |
| * forms of gradual regressions are harder to detect, the measurements become less | |
| * susceptible to actual randomness, because adding artificial noise increases | |
| * the confidence intervals. | |
| */ | |
| trait Noise extends Measurer[Double] { | |
| import exec.noise._ | |
| def noiseFunction(observations: Seq[Double], magnitude: Double): Double => Double | |
| abstract override def measure[T](context: Context, measurements: Int, | |
| setup: T => Any, tear: T => Any, regen: () => T, snippet: T => Any | |
| ): Seq[Quantity[Double]] = { | |
| val observations = super.measure( | |
| context, measurements, setup, tear, regen, snippet) | |
| val magni = context(magnitude) | |
| val noise = noiseFunction(observations.map(_.value), magni) | |
| val withnoise = observations map { | |
| x => x.copy(value = x.value + noise(x.value)) | |
| } | |
| val formatted = withnoise.map(t => f"${t.value}%.3f") | |
| log.verbose("After applying noise: " + formatted.mkString(", ")) | |
| withnoise | |
| } | |
| } | |
| import utils.Statistics.clamp | |
| /** A mixin measurer which adds an absolute amount of Gaussian noise to the | |
| * measurement. | |
| * | |
| * A random value is sampled from a Gaussian distribution for each measurement `x`. | |
| * This value is then multiplied with `Key.noiseMagnitude` and added to the | |
| * measurement. The default value for the noise magnitude is `0.0` - it has to be set | |
| * manually for tests requiring artificial noise. | |
| * The resulting value is clamped into the range `x - magnitude, x + magnitude`. | |
| * | |
| * $noise | |
| */ | |
| trait AbsoluteNoise extends Noise { | |
| abstract override def name = s"${super.name}+AbsoluteNoise" | |
| def noiseFunction(observations: Seq[Double], m: Double) = (x: Double) => { | |
| clamp(m * util.Random.nextGaussian(), -m, +m) | |
| } | |
| } | |
| /** A mixin measurer which adds an amount of Gaussian noise to the measurement | |
| * relative to its mean. | |
| * | |
| * An observations sequence mean `m` is computed. | |
| * A random Gaussian value is sampled for each measurement `x` in the observations | |
| * sequence. It is multiplied with `m / 10.0` times `Key.noiseMagnitude` | |
| * (default `0.0`). Call this multiplication factor `f`. | |
| * The resulting value is clamped into the range `x - f, x + f`. | |
| * | |
| * The bottomline is - a `1.0` noise magnitude is a variation of `10%` of the mean. | |
| * | |
| * $noise | |
| */ | |
| trait RelativeNoise extends Noise { | |
| abstract override def name = s"${super.name}+RelativeNoise" | |
| def noiseFunction(observations: Seq[Double], magnitude: Double) = { | |
| val m = utils.Statistics.mean(observations) | |
| (x: Double) => { | |
| val f = m / 10.0 * magnitude | |
| clamp(f * util.Random.nextGaussian(), -f, +f) | |
| } | |
| } | |
| } | |
| abstract class BaseMemoryFootprint extends Measurer[Double] { | |
| def name = "Measurer.MemoryFootprint" | |
| def measure[T](context: Context, measurements: Int, setup: T => Any, | |
| tear: T => Any, regen: () => T, snippet: T => Any): Seq[Quantity[Double]] = { | |
| val runtime = Runtime.getRuntime | |
| var iteration = 0 | |
| val memories = mutable.ListBuffer.empty[Quantity[Double]] | |
| var value: T = null.asInstanceOf[T] | |
| var obj: Any = null.asInstanceOf[Any] | |
| while (iteration < measurements) { | |
| value = null.asInstanceOf[T] | |
| obj = null.asInstanceOf[T] | |
| Platform.collectGarbage() | |
| val membefore = runtime.totalMemory - runtime.freeMemory | |
| value = regen() | |
| setup(value) | |
| obj = snippet(value) | |
| tear(value) | |
| value = null.asInstanceOf[T] | |
| Platform.collectGarbage() | |
| val memafter = runtime.totalMemory - runtime.freeMemory | |
| val memory = Quantity((memafter - membefore) / 1000.0, "kB") | |
| memories += memory | |
| iteration += 1 | |
| } | |
| log.verbose("Measurements: " + memories.mkString(", ")) | |
| memories.result() | |
| } | |
| } | |
| /** Measures the total memory footprint of an object created by the benchmarking | |
| * snippet. | |
| * | |
| * Eliminates outliers. | |
| */ | |
| class MemoryFootprint extends BaseMemoryFootprint with OutlierElimination[Double] { | |
| override def eliminateLow = true | |
| def numeric: Numeric[Double] = implicitly[Numeric[Double]] | |
| } | |
| class GarbageCollectionCycles extends Measurer[Int] { | |
| def name = "Measurer.GarbageCollectionCycles" | |
| def measure[T](context: Context, measurements: Int, setup: T => Any, | |
| tear: T => Any, regen: () => T, snippet: T => Any): Seq[Quantity[Int]] = { | |
| var iteration = 0 | |
| val gcs = mutable.ListBuffer.empty[Quantity[Int]] | |
| var value: T = null.asInstanceOf[T] | |
| @volatile var count = 0 | |
| while (iteration < measurements) { | |
| value = regen() | |
| count = 0 | |
| setup(value) | |
| Platform.collectGarbage() | |
| utils.withGCNotification { n => | |
| dyn.currentContext.withValue(context) { | |
| log.verbose("GC detected.") | |
| count += 1 | |
| } | |
| } { | |
| snippet(value) | |
| } | |
| tear(value) | |
| value = null.asInstanceOf[T] | |
| gcs += Quantity(count, "#") | |
| iteration += 1 | |
| } | |
| log.verbose("Measurements: " + gcs.mkString(", ")) | |
| gcs.result() | |
| } | |
| } | |
| type Primitive = Boolean with Char with Byte | |
| with Short with Int with Long with Float with Double | |
| /** Counts autoboxed by a Scala compiler values. | |
| * | |
| * @param primitives primitive types whose autoboxing will be counted. | |
| */ | |
| case class BoxingCount(primitives: Class[_ >: Primitive]*) extends InvocationCount { | |
| val matcher: InvocationCountMatcher = { | |
| import InvocationCountMatcher._ | |
| val primitiveToBoxedMethod: Map[Class[_ >: Primitive], String] = Map( | |
| classOf[Boolean] -> "boxToBoolean", | |
| classOf[Char] -> "boxToCharacter", | |
| classOf[Byte] -> "boxToByte", | |
| classOf[Short] -> "boxToShort", | |
| classOf[Int] -> "boxToInteger", | |
| classOf[Long] -> "boxToLong", | |
| classOf[Float] -> "boxToFloat", | |
| classOf[Double] -> "boxToDouble" | |
| ) | |
| InvocationCountMatcher( | |
| classMatcher = ClassMatcher.ClassName(classOf[BoxesRunTime]), | |
| methodMatcher = MethodMatcher.Regex( | |
| new Regex(primitives.map(p => | |
| s"(${primitiveToBoxedMethod(p)})" | |
| ).mkString("^", "|", "$")).pattern | |
| ) | |
| ) | |
| } | |
| def name: String = "Measurer.BoxingCount" | |
| } | |
| object BoxingCount { | |
| /** Creates BoxingCount measurer that counts boxing of all primitive values - | |
| * boolean, char, byte, short, int, long, float and double. | |
| */ | |
| def all() = new BoxingCount(classOf[Boolean], classOf[Char], classOf[Byte], | |
| classOf[Short], classOf[Int], classOf[Long], classOf[Float], classOf[Double]) | |
| /** Creates BoxingCount measurer that counts boxing of all primitive values - | |
| * boolean, char, byte, short, int, long, float and double. | |
| */ | |
| def allWithoutBoolean() = new BoxingCount(classOf[Char], classOf[Byte], | |
| classOf[Short], classOf[Int], classOf[Long], classOf[Float], classOf[Double]) | |
| } | |
| /** Counts invocations of arbitrary method(s) specified by | |
| * [[org.scalameter.execution.invocation.InvocationCountMatcher]]. | |
| */ | |
| case class MethodInvocationCount(matcher: InvocationCountMatcher) | |
| extends InvocationCount { | |
| def name: String = "Measurer.MethodInvocationCount" | |
| } | |
| } |