Skip to content

Commit

Permalink
Add JMH contrib module (#2100)
Browse files Browse the repository at this point in the history
This adds the script from #182 (with minor modifications) as a module under contrib, along with tests and documentation.

Pull request: #2100
  • Loading branch information
lefou authored Nov 4, 2022
2 parents 76eef93 + 2f20466 commit 72ea155
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 0 deletions.
9 changes: 9 additions & 0 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,15 @@ object contrib extends MillModule {
)
}

object jmh extends MillInternalModule with MillAutoTestSetup with WithMillCompiler {
override def compileModuleDeps = Seq(scalalib)
override def testArgs = T {
Seq(
"-DMILL_SCALA_LIB=" + scalalib.runClasspath().map(_.path).mkString(",")
) ++ scalalib.worker.testArgs()
}
override def testModuleDeps: Seq[JavaModule] = super.testModuleDeps ++ Seq(scalalib)
}
}

object scalanativelib extends MillModule {
Expand Down
101 changes: 101 additions & 0 deletions contrib/jmh/src/mill/contrib/jmh/JmhModule.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package mill.contrib.jmh

import mill._, scalalib._, modules._

/**
* This module provides an easy way to integrate <a href="https://openjdk.org/projects/code-tools/jmh/">JMH</a> benchmarking with Mill.
*
* Example configuration:
* {{{
* import mill._, scalalib._
*
* import $ivy.`com.lihaoyi::mill-contrib-jmh:$MILL_VERSION`
* import contrib.jmh.JmhModule
*
* object foo extends ScalaModule with JmhModule {
* def scalaVersion = "2.13.8"
* def jmhCoreVersion = "1.35"
* }
* }}}
*
* Here are some sample commands:
* - mill foo.runJmh # Runs all detected jmh benchmarks
* - mill foo.listJmhBenchmarks # List detected jmh benchmarks
* - mill foo.runJmh -h # List available arguments to runJmh
* - mill foo.runJmh regexp # Run all benchmarks matching `regexp`
*
* For Scala JMH samples see:
* [[https://github.com/sbt/sbt-jmh/tree/main/plugin/src/sbt-test/sbt-jmh/run/src/main/scala/org/openjdk/jmh/samples]].
*/
trait JmhModule extends JavaModule {

def jmhCoreVersion: T[String]
def jmhGeneratorByteCodeVersion: T[String] = jmhCoreVersion

def ivyDeps = super.ivyDeps() ++ Agg(ivy"org.openjdk.jmh:jmh-core:${jmhCoreVersion()}")

def runJmh(args: String*) =
T.command {
val (_, resources) = generateBenchmarkSources()
Jvm.runSubprocess(
"org.openjdk.jmh.Main",
classPath = (runClasspath() ++ generatorDeps()).map(_.path) ++
Seq(compileGeneratedSources().path, resources),
mainArgs = args,
workingDir = T.ctx().dest
)
}

def listJmhBenchmarks(args: String*) = runJmh(("-l" +: args): _*)

def compileGeneratedSources =
T {
val dest = T.ctx().dest
val (sourcesDir, _) = generateBenchmarkSources()
val sources = os.walk(sourcesDir).filter(os.isFile)

os.proc(
Jvm.jdkTool("javac"),
sources.map(_.toString),
"-cp",
(runClasspath() ++ generatorDeps()).map(_.path.toString).mkString(
java.io.File.pathSeparator
),
"-d",
dest
).call(dest)
PathRef(dest)
}

// returns sources and resources directories
def generateBenchmarkSources =
T {
val dest = T.ctx().dest

val sourcesDir = dest / "jmh_sources"
val resourcesDir = dest / "jmh_resources"

os.remove.all(sourcesDir)
os.makeDir.all(sourcesDir)
os.remove.all(resourcesDir)
os.makeDir.all(resourcesDir)

Jvm.runSubprocess(
"org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator",
(runClasspath() ++ generatorDeps()).map(_.path),
mainArgs = Seq(
compile().classes.path.toString,
sourcesDir.toString,
resourcesDir.toString,
"default"
)
)

(sourcesDir, resourcesDir)
}

def generatorDeps =
resolveDeps(
T { Agg(ivy"org.openjdk.jmh:jmh-generator-bytecode:${jmhGeneratorByteCodeVersion()}") }
)
}
34 changes: 34 additions & 0 deletions contrib/jmh/test/resources/jmh/src/Bench1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package mill.contrib.jmh

import org.openjdk.jmh.annotations._

object Bench1States {

@State(Scope.Benchmark)
class BenchmarkState {
@volatile
var x = Math.PI
}

@State(Scope.Thread)
class ThreadState {
@volatile
var x = Math.PI
}
}

@BenchmarkMode(Array(Mode.All))
class Bench1 {

import Bench1States._

@Benchmark
def measureShared(state: BenchmarkState) = {
state.x += 1
}

@Benchmark
def measureUnshared(state: ThreadState) = {
state.x += 1
}
}
18 changes: 18 additions & 0 deletions contrib/jmh/test/resources/jmh/src/Bench2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package mill.contrib.jmh

import org.openjdk.jmh.annotations._
import java.util.concurrent.TimeUnit

@BenchmarkMode(Array(Mode.AverageTime))
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
class Bench2 {

val x = Math.PI

@Benchmark
def sqrt: Double = Math.sqrt(x)

@Benchmark
def log: Double = Math.log(x)
}
51 changes: 51 additions & 0 deletions contrib/jmh/test/src/JmhModuleTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package mill
package contrib.jmh

import mill.api.PathRef
import mill.eval.EvaluatorPaths
import mill.scalalib.ScalaModule
import mill.util.{TestEvaluator, TestUtil}
import os.Path
import utest._
import utest.framework.TestPath

object JmhModuleTest extends TestSuite {

object jmh extends TestUtil.BaseModule with ScalaModule with JmhModule {

override def scalaVersion = sys.props.getOrElse("TEST_SCALA_2_13_VERSION", ???)
override def jmhCoreVersion = "1.35"
override def millSourcePath = TestUtil.getSrcPathBase() / millOuterCtx.enclosing.split('.')
}

val testModuleSourcesPath: Path =
os.pwd / "contrib" / "jmh" / "test" / "resources" / "jmh"

private def workspaceTest(m: TestUtil.BaseModule)(t: TestEvaluator => Unit)(
implicit tp: TestPath
): Unit = {
val eval = new TestEvaluator(m)
os.remove.all(m.millSourcePath)
os.remove.all(eval.outPath)
os.makeDir.all(m.millSourcePath / os.up)
os.copy(testModuleSourcesPath, m.millSourcePath)
t(eval)
}

def tests = Tests {
test("jmh") {
"listJmhBenchmarks" - workspaceTest(jmh) { eval =>
val paths = EvaluatorPaths.resolveDestPaths(eval.outPath, jmh.listJmhBenchmarks())
val outFile = paths.dest / "benchmarks.out"
val Right((result, _)) = eval(jmh.listJmhBenchmarks("-o", outFile.toString))
val expected = """Benchmarks:
|mill.contrib.jmh.Bench2.log
|mill.contrib.jmh.Bench2.sqrt
|mill.contrib.jmh.Bench1.measureShared
|mill.contrib.jmh.Bench1.measureUnshared""".stripMargin
val out = os.read.lines(outFile).map(_.trim).mkString(System.lineSeparator())
assert(out == expected)
}
}
}
}
1 change: 1 addition & 0 deletions docs/antora/modules/ROOT/pages/Contrib_Plugins.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import $ivy.`com.lihaoyi::mill-contrib-bloop:`
* xref:Plugin_Docker.adoc[]
* xref:Plugin_Flyway.adoc[]
* xref:Plugin_Gitlab.adoc[]
* xref:Plugin_Jmh.adoc[]
* xref:Plugin_Play.adoc[]
* xref:Plugin_Proguard.adoc[]
* xref:Plugin_ScalaPB.adoc[]
Expand Down
31 changes: 31 additions & 0 deletions docs/antora/modules/ROOT/pages/Plugin_Jmh.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
= JMH

You can use `JmhModule` to integrate JMH testing with Mill.

Example configuration:

.`build.sc`
[source,scala]
----
import mill._, scalalib._
import $ivy.`com.lihaoyi::mill-contrib-jmh:$MILL_VERSION`
import contrib.jmh.JmhModule
object foo extends ScalaModule with JmhModule {
def scalaVersion = "2.13.8"
def jmhCoreVersion = "1.35"
}
----

Here are some sample commands:

[source,bash]
----
mill foo.runJmh # Runs all detected jmh benchmarks
mill foo.listJmhBenchmarks # List detected jmh benchmarks
mill foo.runJmh -h # List available arguments to runJmh
mill foo.runJmh regexp # Run all benchmarks matching `regexp`
----

For Scala JMH samples see https://github.com/sbt/sbt-jmh/tree/main/plugin/src/sbt-test/sbt-jmh/run/src/main/scala/org/openjdk/jmh/samples[sbt-jmh].

0 comments on commit 72ea155

Please sign in to comment.