Skip to content

Commit 49a0b02

Browse files
authored
Merge pull request #3091 from Gedochao/maintenance/jmh
Tweak benchmarking with JMH
2 parents 915e7cb + e5acdc7 commit 49a0b02

File tree

14 files changed

+226
-88
lines changed

14 files changed

+226
-88
lines changed

build.sc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,10 @@ trait Core extends ScalaCliCrossSbtModule
435435
|
436436
| def localRepoResourcePath = "$localRepoResourcePath"
437437
|
438-
| def jmhVersion = "1.29"
438+
| def jmhVersion = "${Deps.Versions.jmh}"
439+
| def jmhOrg = "${Deps.jmhCore.dep.module.organization.value}"
440+
| def jmhCoreModule = "${Deps.jmhCore.dep.module.name.value}"
441+
| def jmhGeneratorBytecodeModule = "${Deps.jmhGeneratorBytecode.dep.module.name.value}"
439442
|
440443
| def ammoniteVersion = "${Deps.Versions.ammonite}"
441444
| def ammoniteVersionForScala3Lts = "${Deps.Versions.ammoniteForScala3Lts}"

modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import scala.build.*
1111
import scala.build.compiler.{ScalaCompilerMaker, SimpleScalaCompilerMaker}
1212
import scala.build.errors.BuildException
1313
import scala.build.interactive.InteractiveFileOps
14-
import scala.build.internal.Runner
14+
import scala.build.internal.{Constants, Runner}
1515
import scala.build.options.BuildOptions
1616
import scala.cli.CurrentParams
1717
import scala.cli.commands.publish.ConfigUtil.*
@@ -28,6 +28,15 @@ object Doc extends ScalaCommand[DocOptions] {
2828

2929
override def sharedOptions(options: DocOptions): Option[SharedOptions] = Some(options.shared)
3030

31+
override def buildOptions(options: DocOptions): Option[BuildOptions] =
32+
sharedOptions(options)
33+
.map(shared =>
34+
shared.buildOptions(
35+
enableJmh = shared.benchmarking.jmh.getOrElse(false),
36+
jmhVersion = shared.benchmarking.jmhVersion
37+
).orExit(shared.logger)
38+
)
39+
3140
override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroup(HelpGroup.Doc)
3241

3342
override def scalaSpecificationLevel: SpecificationLevel = SpecificationLevel.MUST

modules/cli/src/main/scala/scala/cli/commands/run/Run.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
7474
import options.sharedRun.*
7575
val logger = options.shared.logger
7676
val baseOptions = shared.buildOptions(
77-
enableJmh = benchmarking.jmh.contains(true),
78-
jmhVersion = benchmarking.jmhVersion
77+
enableJmh = shared.benchmarking.jmh.contains(true),
78+
jmhVersion = shared.benchmarking.jmhVersion
7979
).orExit(logger)
8080
baseOptions.copy(
8181
mainClass = mainClass.mainClass,

modules/cli/src/main/scala/scala/cli/commands/run/SharedRunOptions.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import scala.cli.commands.tags
88

99
// format: off
1010
final case class SharedRunOptions(
11-
@Recurse
12-
benchmarking: BenchmarkingOptions = BenchmarkingOptions(),
1311
@Recurse
1412
sharedJava: SharedJavaOptions = SharedJavaOptions(),
1513
@Recurse

modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ final case class SharedOptions(
8787
workspace: SharedWorkspaceOptions = SharedWorkspaceOptions(),
8888
@Recurse
8989
sharedPython: SharedPythonOptions = SharedPythonOptions(),
90+
@Recurse
91+
benchmarking: BenchmarkingOptions = BenchmarkingOptions(),
9092

9193
@Group(HelpGroup.Scala.toString)
9294
@HelpMessage(s"Set the Scala version (${Constants.defaultScalaVersion} by default)")
@@ -422,18 +424,9 @@ final case class SharedOptions(
422424
(ScalaCli.launcherOptions.scalaRunner.cliPredefinedRepository ++ dependencies.repository)
423425
.map(_.trim)
424426
.filter(_.nonEmpty),
425-
extraDependencies = ShadowingSeq.from(
426-
SharedOptions.parseDependencies(
427-
dependencies.dependency.map(Positioned.none),
428-
ignoreErrors
429-
) ++ resolvedToolkitDependency
430-
),
431-
extraCompileOnlyDependencies = ShadowingSeq.from(
432-
SharedOptions.parseDependencies(
433-
dependencies.compileOnlyDependency.map(Positioned.none),
434-
ignoreErrors
435-
) ++ resolvedToolkitDependency
436-
)
427+
extraDependencies = extraDependencies(ignoreErrors, resolvedToolkitDependency),
428+
extraCompileOnlyDependencies =
429+
extraCompileOnlyDependencies(ignoreErrors, resolvedToolkitDependency)
437430
),
438431
internal = bo.InternalOptions(
439432
cache = Some(coursierCache),
@@ -455,6 +448,35 @@ final case class SharedOptions(
455448
)
456449
}
457450

451+
private def resolvedDependencies(
452+
deps: List[String],
453+
ignoreErrors: Boolean,
454+
extraResolvedDependencies: Seq[Positioned[AnyDependency]]
455+
) = ShadowingSeq.from {
456+
SharedOptions.parseDependencies(deps.map(Positioned.none), ignoreErrors) ++
457+
extraResolvedDependencies
458+
}
459+
460+
private def extraCompileOnlyDependencies(
461+
ignoreErrors: Boolean,
462+
resolvedDeps: Seq[Positioned[AnyDependency]]
463+
) = {
464+
val jmhCorePrefix = s"${Constants.jmhOrg}:${Constants.jmhCoreModule}"
465+
val jmhDeps =
466+
if benchmarking.jmh.getOrElse(false) &&
467+
!dependencies.compileOnlyDependency.exists(_.startsWith(jmhCorePrefix)) &&
468+
!dependencies.dependency.exists(_.startsWith(jmhCorePrefix))
469+
then List(s"$jmhCorePrefix:${Constants.jmhVersion}")
470+
else List.empty
471+
val finalDeps = dependencies.compileOnlyDependency ++ jmhDeps
472+
resolvedDependencies(finalDeps, ignoreErrors, resolvedDeps)
473+
}
474+
475+
private def extraDependencies(
476+
ignoreErrors: Boolean,
477+
resolvedDeps: Seq[Positioned[AnyDependency]]
478+
) = resolvedDependencies(dependencies.dependency, ignoreErrors, resolvedDeps)
479+
458480
extension (rawClassPath: List[String]) {
459481
def extractedClassPath: List[os.Path] =
460482
rawClassPath

modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,44 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg
425425
}
426426
}
427427

428+
test("simple jmh") {
429+
val inputs = TestInputs(
430+
os.rel / "benchmark.scala" ->
431+
s"""package bench
432+
|
433+
|import java.util.concurrent.TimeUnit
434+
|import org.openjdk.jmh.annotations._
435+
|
436+
|@BenchmarkMode(Array(Mode.AverageTime))
437+
|@OutputTimeUnit(TimeUnit.NANOSECONDS)
438+
|@Warmup(iterations = 1, time = 100, timeUnit = TimeUnit.MILLISECONDS)
439+
|@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
440+
|@Fork(0)
441+
|class Benchmarks {
442+
|
443+
| @Benchmark
444+
| def foo(): Unit = {
445+
| (1L to 10000000L).sum
446+
| }
447+
|
448+
|}
449+
|""".stripMargin
450+
)
451+
452+
withBsp(inputs, Seq(".", "--power", "--jmh")) { (_, _, remoteServer) =>
453+
async {
454+
val buildTargetsResp = await(remoteServer.workspaceBuildTargets().asScala)
455+
val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq
456+
expect(targets.length == 2)
457+
458+
val compileResult =
459+
await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala)
460+
expect(compileResult.getStatusCode == b.StatusCode.OK)
461+
462+
}
463+
}
464+
}
465+
428466
test("diagnostics") {
429467
val inputs = TestInputs(
430468
os.rel / "Test.scala" ->

modules/integration/src/test/scala/scala/cli/integration/JmhTests.scala

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,105 @@ package scala.cli.integration
22

33
import com.eed3si9n.expecty.Expecty.expect
44

5-
import java.nio.charset.Charset
5+
import java.nio.file.Files
6+
7+
import scala.util.Properties
68

79
class JmhTests extends ScalaCliSuite {
810
override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First
911

12+
lazy val inputs: TestInputs = TestInputs(
13+
os.rel / "benchmark.scala" ->
14+
s"""package bench
15+
|
16+
|import java.util.concurrent.TimeUnit
17+
|import org.openjdk.jmh.annotations._
18+
|
19+
|@BenchmarkMode(Array(Mode.AverageTime))
20+
|@OutputTimeUnit(TimeUnit.NANOSECONDS)
21+
|@Warmup(iterations = 1, time = 100, timeUnit = TimeUnit.MILLISECONDS)
22+
|@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
23+
|@Fork(0)
24+
|class Benchmarks {
25+
|
26+
| @Benchmark
27+
| def foo(): Unit = {
28+
| (1L to 10000000L).sum
29+
| }
30+
|
31+
|}
32+
|""".stripMargin
33+
)
34+
lazy val expectedInOutput = """Result "bench.Benchmarks.foo":"""
35+
1036
test("simple") {
11-
val inputs = TestInputs(
12-
os.rel / "benchmark.scala" ->
13-
s"""package bench
14-
|
15-
|import java.util.concurrent.TimeUnit
16-
|import org.openjdk.jmh.annotations._
17-
|
18-
|@BenchmarkMode(Array(Mode.AverageTime))
19-
|@OutputTimeUnit(TimeUnit.NANOSECONDS)
20-
|@Warmup(iterations = 1, time = 100, timeUnit = TimeUnit.MILLISECONDS)
21-
|@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS)
22-
|@Fork(0)
23-
|class Benchmarks {
24-
|
25-
| @Benchmark
26-
| def foo(): Unit = {
27-
| (1L to 10000000L).sum
28-
| }
29-
|
30-
|}
31-
|""".stripMargin
32-
)
33-
val expectedInOutput = """Result "bench.Benchmarks.foo":"""
37+
// TODO extract running benchmarks to a separate scope, or a separate sub-command
3438
inputs.fromRoot { root =>
3539
val res =
3640
os.proc(TestUtil.cli, "--power", TestUtil.extraOptions, ".", "--jmh").call(cwd = root)
37-
val output = res.out.text(Charset.defaultCharset())
41+
val output = res.out.trim()
3842
expect(output.contains(expectedInOutput))
3943
}
4044
}
4145

46+
test("compile") {
47+
inputs.fromRoot { root =>
48+
os.proc(TestUtil.cli, "compile", "--power", TestUtil.extraOptions, ".", "--jmh")
49+
.call(cwd = root)
50+
}
51+
}
52+
53+
test("doc") {
54+
inputs.fromRoot { root =>
55+
val res = os.proc(TestUtil.cli, "doc", "--power", TestUtil.extraOptions, ".", "--jmh")
56+
.call(cwd = root, stderr = os.Pipe)
57+
expect(!res.err.trim().contains("Error"))
58+
}
59+
}
60+
61+
test("setup-ide") {
62+
// TODO fix setting jmh via a reload & add tests for it
63+
inputs.fromRoot { root =>
64+
os.proc(TestUtil.cli, "setup-ide", "--power", TestUtil.extraOptions, ".", "--jmh")
65+
.call(cwd = root)
66+
}
67+
}
68+
69+
test("package") {
70+
// TODO make package with --jmh build an artifact that actually runs benchmarks
71+
val expectedMessage = "Placeholder main method"
72+
inputs
73+
.add(os.rel / "Main.scala" -> s"""@main def main: Unit = println("$expectedMessage")""")
74+
.fromRoot { root =>
75+
val launcherName = {
76+
val ext = if (Properties.isWin) ".bat" else ""
77+
"launcher" + ext
78+
}
79+
os.proc(
80+
TestUtil.cli,
81+
"package",
82+
"--power",
83+
TestUtil.extraOptions,
84+
".",
85+
"--jmh",
86+
"-o",
87+
launcherName
88+
)
89+
.call(cwd = root)
90+
val launcher = root / launcherName
91+
expect(os.isFile(launcher))
92+
expect(Files.isExecutable(launcher.toNIO))
93+
val output = TestUtil.maybeUseBash(launcher)(cwd = root).out.trim()
94+
expect(output == expectedMessage)
95+
}
96+
}
97+
98+
test("export") {
99+
inputs.fromRoot { root =>
100+
// TODO add proper support for JMH export, we're checking if it doesn't fail the command for now
101+
os.proc(TestUtil.cli, "export", "--power", TestUtil.extraOptions, ".", "--jmh")
102+
.call(cwd = root)
103+
}
104+
}
105+
42106
}

modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,6 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio
1616
_: TestScalaVersion =>
1717
protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions
1818

19-
def maybeUseBash(cmd: os.Shellable*)(cwd: os.Path = null): os.CommandResult = {
20-
val res = os.proc(cmd*).call(cwd = cwd, check = false)
21-
if (Properties.isLinux && res.exitCode == 127)
22-
// /bin/sh seems to have issues with '%' signs in PATH, that coursier can leave
23-
// in the JVM path entry (https://unix.stackexchange.com/questions/126955/percent-in-path-environment-variable)
24-
os.proc((("/bin/bash": os.Shellable) +: cmd)*).call(cwd = cwd)
25-
else {
26-
expect(res.exitCode == 0)
27-
res
28-
}
29-
}
30-
3119
test("simple script") {
3220
val fileName = "simple.sc"
3321
val message = "Hello"
@@ -53,7 +41,7 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio
5341
expect(os.isFile(launcher))
5442
expect(Files.isExecutable(launcher.toNIO))
5543

56-
val output = maybeUseBash(launcher)(cwd = root).out.trim()
44+
val output = TestUtil.maybeUseBash(launcher)(cwd = root).out.trim()
5745
expect(output == message)
5846
}
5947
}
@@ -80,7 +68,7 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio
8068
expect(os.isFile(launcher))
8169
expect(Files.isExecutable(launcher.toNIO))
8270

83-
val output = maybeUseBash(launcher.toString)(cwd = root).out.trim()
71+
val output = TestUtil.maybeUseBash(launcher.toString)(cwd = root).out.trim()
8472
expect(output == message)
8573
}
8674
}
@@ -577,7 +565,7 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio
577565
}
578566
val runnableLauncherSize = os.size(runnableLauncher)
579567

580-
val output = maybeUseBash(runnableLauncher.toString)(cwd = root).out.trim()
568+
val output = TestUtil.maybeUseBash(runnableLauncher.toString)(cwd = root).out.trim()
581569
val maxRunnableLauncherSize = 1024 * 1024 * 12 // should be smaller than 12MB
582570
expect(output == message)
583571
expect(runnableLauncherSize < maxRunnableLauncherSize)
@@ -1132,12 +1120,12 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio
11321120

11331121
// bootstrap
11341122
os.proc(packageCmds).call(cwd = root).out.trim()
1135-
val output = maybeUseBash(launcher.toString)(cwd = root).out.trim()
1123+
val output = TestUtil.maybeUseBash(launcher.toString)(cwd = root).out.trim()
11361124
expect(output == root.toString)
11371125

11381126
// assembly
11391127
os.proc(packageCmds, "--assembly", "-f").call(cwd = root).out.trim()
1140-
val outputAssembly = maybeUseBash(launcher.toString)(cwd = root).out.trim()
1128+
val outputAssembly = TestUtil.maybeUseBash(launcher.toString)(cwd = root).out.trim()
11411129
expect(outputAssembly == root.toString)
11421130
}
11431131
}

modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package scala.cli.integration
22

3+
import com.eed3si9n.expecty.Expecty.expect
4+
35
import java.io.File
46
import java.net.ServerSocket
57
import java.util.Locale
68
import java.util.concurrent.atomic.AtomicInteger
79
import java.util.concurrent.{ExecutorService, Executors, ScheduledExecutorService, ThreadFactory}
810

9-
import scala.Console._
11+
import scala.Console.*
1012
import scala.annotation.tailrec
1113
import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration}
1214
import scala.concurrent.{Await, ExecutionContext, Future}
@@ -324,4 +326,16 @@ object TestUtil {
324326
os.proc("git", "tag", tag).call(cwd = cwd)
325327
println(s"Git initialized at $cwd")
326328
}
329+
330+
def maybeUseBash(cmd: os.Shellable*)(cwd: os.Path = null): os.CommandResult = {
331+
val res = os.proc(cmd*).call(cwd = cwd, check = false)
332+
if (Properties.isLinux && res.exitCode == 127)
333+
// /bin/sh seems to have issues with '%' signs in PATH, that coursier can leave
334+
// in the JVM path entry (https://unix.stackexchange.com/questions/126955/percent-in-path-environment-variable)
335+
os.proc((("/bin/bash": os.Shellable) +: cmd)*).call(cwd = cwd)
336+
else {
337+
expect(res.exitCode == 0)
338+
res
339+
}
340+
}
327341
}

0 commit comments

Comments
 (0)