All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scalameter.japi.JBench.scala Maven / Gradle / Ivy

The newest version!
package org.scalameter.japi



import java.lang.annotation.Annotation
import java.lang.reflect.Method
import org.scalameter.BasePerformanceTest._
import org.scalameter._
import org.scalameter.japi.annotation._
import org.scalameter.picklers.Implicits._
import org.scalameter.picklers.Pickler
import org.scalameter.reporting.{HtmlReporter, RegressionReporter}
import scala.collection.mutable
import scala.language.reflectiveCalls
import scala.util.Try



/** Base class for all annotation-based benchmarks. */
abstract class JBench[U] extends BasePerformanceTest[U] with Serializable {
  private val attachedBenchmarks = mutable.Buffer[JBench[U]]()

  override final def rebuildSetupZipper() = constructSetupTree()

  /** Attaches all the benchmarks from the specified benchmark class to this benchmark.
   */
  def attach(bench: JBench[U]) = attachedBenchmarks += bench

  /** Constructs setup tree.
   */
  private[japi] def constructSetupTree(): Unit = {
    object BenchmarkExtractor {
      def unapply(m: Method): Option[Seq[String]] = {
        Option(m.getAnnotation(classOf[benchmark])).map(_.value().split('.').toSeq)
      }
    }

    val clazz = this.getClass
    val scopedSetups: Seq[(Seq[String], Seq[Method])] = {
      val mapping = mutable.Map.empty[Seq[String], Seq[Method]]
      for (m @ BenchmarkExtractor(scopes) <- clazz.getMethods) {
        mapping.get(scopes) match {
          case Some(setups) =>
            mapping += (scopes -> (setups :+ m))
          case None =>
            mapping += (scopes -> Seq(m))
        }
      }
      mapping.toList
    }
    val defaultCtx: Context =
      getFieldOrMethod(clazz, "defaultConfig", "Class").asInstanceOf[Context]
    val scopeCtxs = {
      Option(clazz.getAnnotation(classOf[scopes])).map { a =>
        a.value().map { sc =>
          sc.scope().split('.').toSeq -> {
            getFieldOrMethod(clazz, sc.context(),
              s"'scopeCtx' in the `scopes` annotation over ${clazz.getSimpleName}"
            ).asInstanceOf[Context]
          }
        }.toMap
      }.getOrElse(Map.empty)
    }
    for ((scope, mapping) <- scopedSetups.groupBy(_._1.head)) {
      setScope(
        clazz, defaultCtx, scopeCtxs, scope, mapping.map(kv => (kv._1.tail, kv._2))
      )
    }

    // Include benchmarks from attached benchmark classes.
    for (bench <- attachedBenchmarks) bench.constructSetupTree()
  }

  /** Extends the scope named `name` with the corresponding context specified in
   *  `scopeCtxs` and adds the corresponding benchmark snippets in `scopedSetups`.
   *
   *  After this method is invoked, the setup zipper is set to a setup tree
   *  that contains all the benchmark snippets.
   */
  private def setScope(
    clazz: Class[_], defaultCtx: Context, scopeCtxs: Map[Seq[String], Context],
    name: String, scopedSetups: Seq[(Seq[String], Seq[Method])]
  ): Unit = {
    val scope = Scope(name, setupzipper.value.current.context)
    scope config {
      defaultCtx ++ scopeCtxs.getOrElse(
        (scope.name :: scope.context(Key.dsl.scope)).reverse, Context.empty
      )
    } in {
      scopedSetups.groupBy(_._1.headOption).foreach {
        case (None, newMapping) =>
          for ((_, ms) <- newMapping; m <- ms) {
            setSetup(clazz, m)
          }
        case (Some(newScope), newMapping) =>
          setScope(
            clazz, defaultCtx, scopeCtxs, newScope,
            newMapping.map(kv => (kv._1.tail, kv._2))
          )
      }
    }
  }

  /** Sets setup for a benchmark snippet.
   */
  private def setSetup(cl: Class[_], m: Method): Unit = {
    val additionalContext = Option(m.getAnnotation(classOf[ctx])).map(a =>
      getFieldOrMethod(cl, a.value(),
        s"'ctx' annotation over '${m.getName}' method").asInstanceOf[Context]
    ).getOrElse(Context.empty)

    if (Option(m.getAnnotation(classOf[disabled])) != None) {
      // Benchmark is disabled.
      return
    }

    val gen = Option(m.getAnnotation(classOf[gen])).map(a =>
      getFieldOrMethod(cl, a.value(),
        s"'gen' annotation over '${m.getName}' method") match {
        case jgen: JGen[_] => jgen.asScala().asInstanceOf[Gen[AnyRef]]
        case gen: Gen[_] => gen.asInstanceOf[Gen[AnyRef]]
        case other => sys.error(s"Unknown generator type in '${a.value}. " +
          s"Expected JGen or Gen. Got ${other.getClass.getSimpleName}'.")
      }
    ).getOrElse(sys.error("Each benchmark method should be annotated with 'gen'."))

    val setupBeforeAll = getNoArgMethod(m, classOf[setupBeforeAll], cl)
      .asInstanceOf[Option[() => Unit]]

    val teardownAfterAll = getNoArgMethod(m, classOf[teardownAfterAll], cl)
      .asInstanceOf[Option[() => Unit]]

    val setup: Option[AnyRef => Any] = getOneArgMethod(m, classOf[setup], cl)

    val teardown: Option[AnyRef => Any] = getOneArgMethod(m, classOf[teardown], cl)

    val warmup: Option[() => Any] = getNoArgMethod(m, classOf[warmup], cl)

    val snippet: AnyRef => Any = {
      m.setAccessible(true)
      val sm = new SerializableMethod(m)
      v => sm.invokeA(this, v)
    }

    val curveName = Option(m.getAnnotation(classOf[curve]))
      .map(_.value()).getOrElse(m.getName)

    val context = setupzipper.value.current.context +
      (Key.dsl.curve -> curveName) ++ additionalContext

    val benchmark = Setup[AnyRef](
      context = context,
      gen = gen,
      setupbeforeall = setupBeforeAll,
      teardownafterall = teardownAfterAll,
      setup = setup,
      teardown = teardown,
      customwarmup = warmup,
      snippet = snippet
    )

    setupzipper.value = setupzipper.value.addItem(benchmark)
  }

  /** Gets value from a field or no-arg method.
   */
  private def getFieldOrMethod(
    selfClass: Class[_], name: String, errorPrfx: String
  ): Any = {
    Try(selfClass.getField(name).get(this)) orElse
    Try(selfClass.getMethod(name).invoke(this)) getOrElse
    sys.error(
      s"$errorPrfx is referring to a non-existent field or 0-arg method $name"
    )
  }

  /** Returns no-arg method pointed by given annotation.
   */
  private def getNoArgMethod[A <: Annotation { def value(): String }](
    from: Method, annotationClass: Class[A], selfClass: Class[_]
  ): Option[() => Any] = {
    Option(from.getAnnotation(annotationClass)).map { a =>
      val m = selfClass.getMethod(a.value())
      m.setAccessible(true)
      val sm = new SerializableMethod(m)
      () => sm.invoke(this)
    }
  }

  /** Returns one-arg method pointed by given annotation.
   */
  private def getOneArgMethod[A <: Annotation { def value(): String }](
    from: Method, annotationClass: Class[A], selfClass: Class[_]
  ): Option[AnyRef => Any] = {
    Option(from.getAnnotation(annotationClass)).map { a =>
      val m = selfClass.getMethods.find(_.getName == a.value())
        .getOrElse(throw new NoSuchMethodException(a.value()))
      require(m.getParameterTypes.length == 1,
        s"Expected method ${a.value()} to have single argument. " +
          s"Got ${m.getParameterTypes.length} arguments.")
      m.setAccessible(true)
      val sm = new SerializableMethod(m)
      v => sm.invokeA(this, v)
    }
  }
}

object JBench {
  /** Annotation based equivalent of the [[org.scalameter.Bench.Local]] */
  abstract class Local[U: Numeric: Pickler] extends JBench[U] {
    def warmer: Warmer = new Warmer.Default

    def aggregator: Aggregator[U]

    def executor: Executor[U] = new execution.LocalExecutor(
      warmer,
      aggregator,
      measurer
    )

    def persistor: Persistor = Persistor.None

    def reporter: Reporter[U] = new RegressionReporter(tester, historian)

    def historian: RegressionReporter.Historian =
      RegressionReporter.Historian.ExponentialBackoff()

    def tester: RegressionReporter.Tester =
      RegressionReporter.Tester.Accepter()
  }

  /** Annotation based equivalent of the [[org.scalameter.Bench.Forked]] */
  abstract class Forked[U: Numeric: Pickler: PrettyPrinter] extends JBench[U] {
    def warmer: Warmer = new Warmer.Default

    def aggregator: Aggregator[U]

    def executor: Executor[U] = new execution.SeparateJvmsExecutor(
      warmer,
      aggregator,
      measurer
    )

    def persistor: Persistor = Persistor.None

    def reporter: Reporter[U] = new RegressionReporter(tester, historian)

    def historian: RegressionReporter.Historian =
      RegressionReporter.Historian.ExponentialBackoff()

    def tester: RegressionReporter.Tester =
      RegressionReporter.Tester.Accepter()
  }

  /** Annotation based equivalent of the [[org.scalameter.Bench.Persisted]] */
  abstract class Persisted[U: Pickler: PrettyPrinter] extends JBench[U] {
    def warmer: Warmer = new Warmer.Default

    def aggregator: Aggregator[U]

    def executor: Executor[U] = new execution.SeparateJvmsExecutor[U](
      warmer,
      aggregator,
      measurer
    )

    def persistor: Persistor = new persistence.GZIPJSONSerializationPersistor
  }

  /** Annotation based equivalent of the [[org.scalameter.Bench.LocalTime]] */
  abstract class LocalTime extends Local[Double] {
    def aggregator: Aggregator[Double] = Aggregator.min

    def measurer: Measurer[Double] = new Measurer.Default()
  }

  /** Annotation based equivalent of the [[org.scalameter.Bench.ForkedTime]] */
  @deprecated("Please use ForkedStableTime instead.", "0.16")
  abstract class ForkedTime extends Forked[Double] {
    def aggregator: Aggregator[Double] = Aggregator.min

    def measurer: Measurer[Double] =
      new Measurer.IgnoringGC with Measurer.PeriodicReinstantiation[Double] {
        override val defaultFrequency = 12
        override val defaultFullGC = true
      }
  }

  abstract class ForkedStableTime extends Forked[Double] {
    def aggregator: Aggregator[Double] = Aggregator.min

    def measurer: Measurer[Double] =
      new Measurer.IgnoringGC with Measurer.PeriodicReinstantiation[Double] {
        override val defaultFrequency = 12
        override val defaultFullGC = true
      }
  }

  abstract class ForkedPreciseTime extends Forked[Double] {
    def aggregator: Aggregator[Double] = Aggregator.average

    def measurer: Measurer[Double] = new Measurer.Default
  }

  /** Annotation base equivalent of the [[org.scalameter.Bench.HTMLReport]] */
  abstract class HTMLReport extends Persisted[Double] {
    def aggregator: Aggregator[Double] = Aggregator.average

    def measurer: Measurer[Double] =
      new Measurer.IgnoringGC with Measurer.PeriodicReinstantiation[Double]
        with Measurer.OutlierElimination[Double] with Measurer.RelativeNoise {
        def numeric: Numeric[Double] = implicitly[Numeric[Double]]
      }

    def reporter: Reporter[Double] = Reporter.Composite(
      new RegressionReporter(tester, historian),
      HtmlReporter(!online)
    )

    def historian: RegressionReporter.Historian

    def online: Boolean

    def tester: RegressionReporter.Tester
  }

  /** Annotation base equivalent of the [[org.scalameter.Bench.OnlineRegressionReport]]
   */
  abstract class OnlineRegressionReport extends HTMLReport {
    def historian: RegressionReporter.Historian =
      RegressionReporter.Historian.ExponentialBackoff()

    def online = true

    def tester: RegressionReporter.Tester =
      RegressionReporter.Tester.OverlapIntervals()
  }

  /** Annotation base equivalent of the [[org.scalameter.Bench.OfflineRegressionReport]]
   */
  abstract class OfflineRegressionReport extends HTMLReport {
    def historian: RegressionReporter.Historian =
      RegressionReporter.Historian.ExponentialBackoff()

    def online = false

    def tester: RegressionReporter.Tester =
      RegressionReporter.Tester.OverlapIntervals()
  }

  /** Annotation base equivalent of the [[org.scalameter.Bench.OfflineReport]] */
  abstract class OfflineReport extends HTMLReport {
    def historian: RegressionReporter.Historian =
      RegressionReporter.Historian.ExponentialBackoff()

    def online = false

    def tester: RegressionReporter.Tester =
      RegressionReporter.Tester.Accepter()
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy