diff --git a/build.sc b/build.sc index 839cce0cfd..36936669af 100644 --- a/build.sc +++ b/build.sc @@ -1045,6 +1045,11 @@ trait CliIntegration extends SbtModule with ScalaCliPublishModule with HasTests | | def ghOrg = "$ghOrg" | def ghName = "$ghName" + | + | def jmhVersion = "${Deps.Versions.jmh}" + | def jmhOrg = "${Deps.jmhCore.dep.module.organization.value}" + | def jmhCoreModule = "${Deps.jmhCore.dep.module.name.value}" + | def jmhGeneratorBytecodeModule = "${Deps.jmhGeneratorBytecode.dep.module.name.value}" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index b97e9f4592..975db8f226 100644 --- a/modules/build/src/main/scala/scala/build/Build.scala +++ b/modules/build/src/main/scala/scala/build/Build.scala @@ -451,7 +451,7 @@ object Build { build0 match { case successful: Successful => - if (options.jmhOptions.runJmh.getOrElse(false) && scope == Scope.Main) + if (options.jmhOptions.canRunJmh && scope == Scope.Main) value { val res = jmhBuild( inputs, diff --git a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala index 1201bf3b71..e3ed9ba4da 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala @@ -12,6 +12,7 @@ import scala.build.preprocessing.directives object DirectivesPreprocessingUtils { val usingDirectiveHandlers: Seq[DirectiveHandler[BuildOptions]] = Seq[DirectiveHandler[_ <: HasBuildOptions]]( + directives.Benchmarking.handler, directives.BuildInfo.handler, directives.ComputeVersion.handler, directives.Exclude.handler, diff --git a/modules/cli/src/main/scala/scala/cli/commands/bloop/Bloop.scala b/modules/cli/src/main/scala/scala/cli/commands/bloop/Bloop.scala index f139163132..01c42a1d4c 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/bloop/Bloop.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/bloop/Bloop.scala @@ -33,7 +33,7 @@ object Bloop extends ScalaCommand[BloopOptions] { jvm = opts.jvm, coursier = opts.coursier ) - val options = sharedOptions.buildOptions(false, None).orExit(opts.global.logging.logger) + val options = sharedOptions.buildOptions().orExit(opts.global.logging.logger) val javaHomeInfo = opts.compilationServer.bloopJvm .map(JvmUtils.downloadJvm(_, options)) diff --git a/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala b/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala index 42a3c876ae..abb5f5834e 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/doc/Doc.scala @@ -30,12 +30,7 @@ object Doc extends ScalaCommand[DocOptions] { override def buildOptions(options: DocOptions): Option[BuildOptions] = sharedOptions(options) - .map(shared => - shared.buildOptions( - enableJmh = shared.benchmarking.jmh.getOrElse(false), - jmhVersion = shared.benchmarking.jmhVersion - ).orExit(shared.logger) - ) + .map(shared => shared.buildOptions().orExit(shared.logger)) override def helpFormat: HelpFormat = super.helpFormat.withPrimaryGroup(HelpGroup.Doc) diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index 2c5e71ea98..cdb997af3d 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -72,11 +72,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { override def buildOptions(options: RunOptions): Some[BuildOptions] = Some { import options.* import options.sharedRun.* - val logger = options.shared.logger - val baseOptions = shared.buildOptions( - enableJmh = shared.benchmarking.jmh.contains(true), - jmhVersion = shared.benchmarking.jmhVersion - ).orExit(logger) + val logger = options.shared.logger + val baseOptions = shared.buildOptions().orExit(logger) baseOptions.copy( mainClass = mainClass.mainClass, javaOptions = baseOptions.javaOptions.copy( @@ -375,7 +372,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { val mainClassOpt = build.options.mainClass.filter(_.nonEmpty) // trim it too? .orElse { - if (build.options.jmhOptions.runJmh.contains(false)) Some("org.openjdk.jmh.Main") + if build.options.jmhOptions.enableJmh.contains(true) && !build.options.jmhOptions.canRunJmh + then Some("org.openjdk.jmh.Main") else None } val mainClass = mainClassOpt match { diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/BenchmarkingOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/BenchmarkingOptions.scala index fe0649fc1a..6d0ed1a579 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/BenchmarkingOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/BenchmarkingOptions.scala @@ -2,6 +2,7 @@ package scala.cli.commands.shared import caseapp.* +import scala.build.internal.Constants import scala.cli.commands.tags // format: off @@ -12,7 +13,7 @@ final case class BenchmarkingOptions( jmh: Option[Boolean] = None, @Group(HelpGroup.Benchmarking.toString) @Tag(tags.experimental) - @HelpMessage("Set JMH version") + @HelpMessage(s"Set JMH version (default: ${Constants.jmhVersion})") @ValueDescription("version") jmhVersion: Option[String] = None ) diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala index 0f69a99020..4d1d039099 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala @@ -301,152 +301,148 @@ final case class SharedOptions( def scalacOptions: List[String] = scalac.scalacOption ++ scalacOptionsFromFiles - def buildOptions( - enableJmh: Boolean = false, - jmhVersion: Option[String] = None, - ignoreErrors: Boolean = false - ): Either[BuildException, bo.BuildOptions] = either { - val releaseOpt = scalacOptions.getScalacOption("-release") - val targetOpt = scalacOptions.getScalacPrefixOption("-target") - jvm.jvm -> (releaseOpt.toSeq ++ targetOpt) match { - case (Some(j), compilerTargets) if compilerTargets.exists(_ != j) => - val compilerTargetsString = compilerTargets.distinct.mkString(", ") - logger.error( - s"Warning: different target JVM ($j) and scala compiler target JVM ($compilerTargetsString) were passed." - ) - case _ => - } - val parsedPlatform = platform.map(Platform.normalize).flatMap(Platform.parse) - val platformOpt = value { - (parsedPlatform, js.js, native.native) match { - case (Some(p: Platform.JS.type), _, false) => Right(Some(p)) - case (Some(p: Platform.Native.type), false, _) => Right(Some(p)) - case (Some(p: Platform.JVM.type), false, false) => Right(Some(p)) - case (Some(p), _, _) => - val jsSeq = if (js.js) Seq(Platform.JS) else Seq.empty - val nativeSeq = if (native.native) Seq(Platform.Native) else Seq.empty - val platformsSeq = Seq(p) ++ jsSeq ++ nativeSeq - Left(new AmbiguousPlatformError(platformsSeq.distinct.map(_.toString))) - case (_, true, true) => - Left(new AmbiguousPlatformError(Seq(Platform.JS.toString, Platform.Native.toString))) - case (_, true, _) => Right(Some(Platform.JS)) - case (_, _, true) => Right(Some(Platform.Native)) - case _ => Right(None) + def buildOptions(ignoreErrors: Boolean = false): Either[BuildException, bo.BuildOptions] = + either { + val releaseOpt = scalacOptions.getScalacOption("-release") + val targetOpt = scalacOptions.getScalacPrefixOption("-target") + jvm.jvm -> (releaseOpt.toSeq ++ targetOpt) match { + case (Some(j), compilerTargets) if compilerTargets.exists(_ != j) => + val compilerTargetsString = compilerTargets.distinct.mkString(", ") + logger.error( + s"Warning: different target JVM ($j) and scala compiler target JVM ($compilerTargetsString) were passed." + ) + case _ => } - } - val (assumedSourceJars, extraRegularJarsAndClasspath) = - extraJarsAndClassPath.partition(_.hasSourceJarSuffix) - if assumedSourceJars.nonEmpty then - val assumedSourceJarsString = assumedSourceJars.mkString(", ") - logger.message( - s"""[${Console.YELLOW}warn${Console.RESET}] Jars with the ${ScalaCliConsole - .GRAY}*-sources.jar${Console.RESET} name suffix are assumed to be source jars. - |The following jars were assumed to be source jars and will be treated as such: $assumedSourceJarsString""".stripMargin - ) - val (resolvedToolkitDependency, toolkitMaxDefaultScalaNativeVersions) = - SharedOptions.resolveToolkitDependencyAndScalaNativeVersionReqs(withToolkit, logger) - val scalapyMaxDefaultScalaNativeVersions = - if sharedPython.python.contains(true) then - List(Constants.scalaPyMaxScalaNative -> Python.maxScalaNativeWarningMsg) - else Nil - val maxDefaultScalaNativeVersions = - toolkitMaxDefaultScalaNativeVersions.toList ++ scalapyMaxDefaultScalaNativeVersions - val snOpts = scalaNativeOptions(native, maxDefaultScalaNativeVersions) - bo.BuildOptions( - sourceGeneratorOptions = bo.SourceGeneratorOptions( - useBuildInfo = sourceGenerator.useBuildInfo, - projectVersion = sharedVersionOptions.projectVersion, - computeVersion = value { - sharedVersionOptions.computeVersion - .map(Positioned.commandLine) - .map(ComputeVersion.parse) - .sequence + val parsedPlatform = platform.map(Platform.normalize).flatMap(Platform.parse) + val platformOpt = value { + (parsedPlatform, js.js, native.native) match { + case (Some(p: Platform.JS.type), _, false) => Right(Some(p)) + case (Some(p: Platform.Native.type), false, _) => Right(Some(p)) + case (Some(p: Platform.JVM.type), false, false) => Right(Some(p)) + case (Some(p), _, _) => + val jsSeq = if (js.js) Seq(Platform.JS) else Seq.empty + val nativeSeq = if (native.native) Seq(Platform.Native) else Seq.empty + val platformsSeq = Seq(p) ++ jsSeq ++ nativeSeq + Left(new AmbiguousPlatformError(platformsSeq.distinct.map(_.toString))) + case (_, true, true) => + Left(new AmbiguousPlatformError(Seq(Platform.JS.toString, Platform.Native.toString))) + case (_, true, _) => Right(Some(Platform.JS)) + case (_, _, true) => Right(Some(Platform.Native)) + case _ => Right(None) } - ), - suppressWarningOptions = - bo.SuppressWarningOptions( - suppressDirectivesInMultipleFilesWarning = getOptionOrFromConfig( - suppress.suppressDirectivesInMultipleFilesWarning, - Keys.suppressDirectivesInMultipleFilesWarning + } + val (assumedSourceJars, extraRegularJarsAndClasspath) = + extraJarsAndClassPath.partition(_.hasSourceJarSuffix) + if assumedSourceJars.nonEmpty then + val assumedSourceJarsString = assumedSourceJars.mkString(", ") + logger.message( + s"""[${Console.YELLOW}warn${Console.RESET}] Jars with the ${ScalaCliConsole + .GRAY}*-sources.jar${Console.RESET} name suffix are assumed to be source jars. + |The following jars were assumed to be source jars and will be treated as such: $assumedSourceJarsString""".stripMargin + ) + val (resolvedToolkitDependency, toolkitMaxDefaultScalaNativeVersions) = + SharedOptions.resolveToolkitDependencyAndScalaNativeVersionReqs(withToolkit, logger) + val scalapyMaxDefaultScalaNativeVersions = + if sharedPython.python.contains(true) then + List(Constants.scalaPyMaxScalaNative -> Python.maxScalaNativeWarningMsg) + else Nil + val maxDefaultScalaNativeVersions = + toolkitMaxDefaultScalaNativeVersions.toList ++ scalapyMaxDefaultScalaNativeVersions + val snOpts = scalaNativeOptions(native, maxDefaultScalaNativeVersions) + bo.BuildOptions( + sourceGeneratorOptions = bo.SourceGeneratorOptions( + useBuildInfo = sourceGenerator.useBuildInfo, + projectVersion = sharedVersionOptions.projectVersion, + computeVersion = value { + sharedVersionOptions.computeVersion + .map(Positioned.commandLine) + .map(ComputeVersion.parse) + .sequence + } + ), + suppressWarningOptions = + bo.SuppressWarningOptions( + suppressDirectivesInMultipleFilesWarning = getOptionOrFromConfig( + suppress.suppressDirectivesInMultipleFilesWarning, + Keys.suppressDirectivesInMultipleFilesWarning + ), + suppressOutdatedDependencyWarning = getOptionOrFromConfig( + suppress.suppressOutdatedDependencyWarning, + Keys.suppressOutdatedDependenciessWarning + ), + suppressExperimentalFeatureWarning = getOptionOrFromConfig( + suppress.global.suppressExperimentalFeatureWarning, + Keys.suppressExperimentalFeatureWarning + ) ), - suppressOutdatedDependencyWarning = getOptionOrFromConfig( - suppress.suppressOutdatedDependencyWarning, - Keys.suppressOutdatedDependenciessWarning + scalaOptions = bo.ScalaOptions( + scalaVersion = scalaVersion + .map(_.trim) + .filter(_.nonEmpty) + .map(bo.MaybeScalaVersion(_)), + scalaBinaryVersion = scalaBinaryVersion.map(_.trim).filter(_.nonEmpty), + addScalaLibrary = scalaLibrary.orElse(java.map(!_)), + addScalaCompiler = withCompiler, + semanticDbOptions = bo.SemanticDbOptions( + generateSemanticDbs = semanticDbOptions.semanticDb, + semanticDbTargetRoot = semanticDbOptions.semanticDbTargetRoot.map(os.Path(_, os.pwd)), + semanticDbSourceRoot = semanticDbOptions.semanticDbSourceRoot.map(os.Path(_, os.pwd)) ), - suppressExperimentalFeatureWarning = getOptionOrFromConfig( - suppress.global.suppressExperimentalFeatureWarning, - Keys.suppressExperimentalFeatureWarning - ) + scalacOptions = scalacOptions + .withScalacExtraOptions(scalacExtra) + .toScalacOptShadowingSeq + .filterNonRedirected + .filterNonDeprecated + .map(Positioned.commandLine), + compilerPlugins = + SharedOptions.parseDependencies( + dependencies.compilerPlugin.map(Positioned.none), + ignoreErrors + ), + platform = platformOpt.map(o => Positioned(List(Position.CommandLine()), o)) ), - scalaOptions = bo.ScalaOptions( - scalaVersion = scalaVersion - .map(_.trim) - .filter(_.nonEmpty) - .map(bo.MaybeScalaVersion(_)), - scalaBinaryVersion = scalaBinaryVersion.map(_.trim).filter(_.nonEmpty), - addScalaLibrary = scalaLibrary.orElse(java.map(!_)), - addScalaCompiler = withCompiler, - semanticDbOptions = bo.SemanticDbOptions( - generateSemanticDbs = semanticDbOptions.semanticDb, - semanticDbTargetRoot = semanticDbOptions.semanticDbTargetRoot.map(os.Path(_, os.pwd)), - semanticDbSourceRoot = semanticDbOptions.semanticDbSourceRoot.map(os.Path(_, os.pwd)) + scriptOptions = bo.ScriptOptions( + forceObjectWrapper = objectWrapper ), - scalacOptions = scalacOptions - .withScalacExtraOptions(scalacExtra) - .toScalacOptShadowingSeq - .filterNonRedirected - .filterNonDeprecated - .map(Positioned.commandLine), - compilerPlugins = - SharedOptions.parseDependencies( - dependencies.compilerPlugin.map(Positioned.none), - ignoreErrors - ), - platform = platformOpt.map(o => Positioned(List(Position.CommandLine()), o)) - ), - scriptOptions = bo.ScriptOptions( - forceObjectWrapper = objectWrapper - ), - scalaJsOptions = scalaJsOptions(js), - scalaNativeOptions = snOpts, - javaOptions = value(scala.cli.commands.util.JvmUtils.javaOptions(jvm)), - jmhOptions = bo.JmhOptions( - addJmhDependencies = - if (enableJmh) jmhVersion.orElse(Some(Constants.jmhVersion)) - else None, - runJmh = if (enableJmh) Some(true) else None - ), - classPathOptions = bo.ClassPathOptions( - extraClassPath = extraRegularJarsAndClasspath, - extraCompileOnlyJars = extraCompileOnlyClassPath, - extraSourceJars = extraSourceJars.extractedClassPath ++ assumedSourceJars, - extraRepositories = - (ScalaCli.launcherOptions.scalaRunner.cliPredefinedRepository ++ dependencies.repository) - .map(_.trim) - .filter(_.nonEmpty), - extraDependencies = extraDependencies(ignoreErrors, resolvedToolkitDependency), - extraCompileOnlyDependencies = - extraCompileOnlyDependencies(ignoreErrors, resolvedToolkitDependency) - ), - internal = bo.InternalOptions( - cache = Some(coursierCache), - localRepository = LocalRepo.localRepo(Directories.directories.localRepoDir, logger), - verbosity = Some(logging.verbosity), - strictBloopJsonCheck = strictBloopJsonCheck, - interactive = Some(() => interactive), - exclude = exclude.map(Positioned.commandLine), - offline = coursier.getOffline() - ), - notForBloopOptions = bo.PostBuildOptions( - scalaJsLinkerOptions = linkerOptions(js), - addRunnerDependencyOpt = runner, - python = sharedPython.python, - pythonSetup = sharedPython.pythonSetup, - scalaPyVersion = sharedPython.scalaPyVersion - ), - useBuildServer = compilationServer.server - ) - } + scalaJsOptions = scalaJsOptions(js), + scalaNativeOptions = snOpts, + javaOptions = value(scala.cli.commands.util.JvmUtils.javaOptions(jvm)), + jmhOptions = bo.JmhOptions( + jmhVersion = benchmarking.jmhVersion, + enableJmh = benchmarking.jmh, + runJmh = benchmarking.jmh + ), + classPathOptions = bo.ClassPathOptions( + extraClassPath = extraRegularJarsAndClasspath, + extraCompileOnlyJars = extraCompileOnlyClassPath, + extraSourceJars = extraSourceJars.extractedClassPath ++ assumedSourceJars, + extraRepositories = + (ScalaCli.launcherOptions.scalaRunner.cliPredefinedRepository ++ dependencies.repository) + .map(_.trim) + .filter(_.nonEmpty), + extraDependencies = extraDependencies(ignoreErrors, resolvedToolkitDependency), + extraCompileOnlyDependencies = + extraCompileOnlyDependencies(ignoreErrors, resolvedToolkitDependency) + ), + internal = bo.InternalOptions( + cache = Some(coursierCache), + localRepository = LocalRepo.localRepo(Directories.directories.localRepoDir, logger), + verbosity = Some(logging.verbosity), + strictBloopJsonCheck = strictBloopJsonCheck, + interactive = Some(() => interactive), + exclude = exclude.map(Positioned.commandLine), + offline = coursier.getOffline() + ), + notForBloopOptions = bo.PostBuildOptions( + scalaJsLinkerOptions = linkerOptions(js), + addRunnerDependencyOpt = runner, + python = sharedPython.python, + pythonSetup = sharedPython.pythonSetup, + scalaPyVersion = sharedPython.scalaPyVersion + ), + useBuildServer = compilationServer.server + ) + } private def resolvedDependencies( deps: List[String], @@ -460,17 +456,7 @@ final case class SharedOptions( private def extraCompileOnlyDependencies( ignoreErrors: Boolean, resolvedDeps: Seq[Positioned[AnyDependency]] - ) = { - val jmhCorePrefix = s"${Constants.jmhOrg}:${Constants.jmhCoreModule}" - val jmhDeps = - if benchmarking.jmh.getOrElse(false) && - !dependencies.compileOnlyDependency.exists(_.startsWith(jmhCorePrefix)) && - !dependencies.dependency.exists(_.startsWith(jmhCorePrefix)) - then List(s"$jmhCorePrefix:${Constants.jmhVersion}") - else List.empty - val finalDeps = dependencies.compileOnlyDependency ++ jmhDeps - resolvedDependencies(finalDeps, ignoreErrors, resolvedDeps) - } + ) = resolvedDependencies(dependencies.compileOnlyDependency, ignoreErrors, resolvedDeps) private def extraDependencies( ignoreErrors: Boolean, @@ -580,7 +566,7 @@ final case class SharedOptions( def bloopRifleConfig(extraBuildOptions: Option[BuildOptions] = None) : Either[BuildException, BloopRifleConfig] = either { - val options = extraBuildOptions.foldLeft(value(buildOptions(false, None)))(_ orElse _) + val options = extraBuildOptions.foldLeft(value(buildOptions()))(_ orElse _) lazy val defaultJvmHome = value { JvmUtils.downloadJvm(OsLibc.defaultJvm(OsLibc.jvmIndexOs), options) } diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Benchmarking.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Benchmarking.scala new file mode 100644 index 0000000000..0031812b89 --- /dev/null +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Benchmarking.scala @@ -0,0 +1,47 @@ +package scala.build.preprocessing.directives + +import dependency.* + +import scala.build.directives.* +import scala.build.errors.BuildException +import scala.build.internal.Constants +import scala.build.options.{ + BuildOptions, + ClassPathOptions, + JmhOptions, + ScalaNativeOptions, + ShadowingSeq +} +import scala.build.{Positioned, options} +import scala.cli.commands.SpecificationLevel + +@DirectiveGroupName("Benchmarking options") +@DirectiveExamples(s"//> using jmhVersion ${Constants.jmhVersion}") +@DirectiveUsage( + "//> using jmh _value_ | using jmhVersion _value_", + """`//> using jmh` _value_ + | + |`//> using jmhVersion` _value_ + """.stripMargin.trim +) +@DirectiveDescription("Add benchmarking options") +@DirectiveLevel(SpecificationLevel.EXPERIMENTAL) +case class Benchmarking( + jmh: Option[Boolean] = None, + jmhVersion: Option[Positioned[String]] = None +) extends HasBuildOptions { + def buildOptions: Either[BuildException, BuildOptions] = + Right( + BuildOptions( + jmhOptions = JmhOptions( + enableJmh = jmh, + runJmh = jmh, + jmhVersion = jmhVersion.map(_.value) + ) + ) + ) +} + +object Benchmarking { + val handler: DirectiveHandler[Benchmarking] = DirectiveHandler.derive +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala index ba7634c232..593cbb237b 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala @@ -263,44 +263,6 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg } } - test("simple jmh") { - val inputs = TestInputs( - os.rel / "benchmark.scala" -> - s"""package bench - | - |import java.util.concurrent.TimeUnit - |import org.openjdk.jmh.annotations._ - | - |@BenchmarkMode(Array(Mode.AverageTime)) - |@OutputTimeUnit(TimeUnit.NANOSECONDS) - |@Warmup(iterations = 1, time = 100, timeUnit = TimeUnit.MILLISECONDS) - |@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS) - |@Fork(0) - |class Benchmarks { - | - | @Benchmark - | def foo(): Unit = { - | (1L to 10000000L).sum - | } - | - |} - |""".stripMargin - ) - - withBsp(inputs, Seq(".", "--power", "--jmh")) { (_, _, remoteServer) => - async { - val buildTargetsResp = await(remoteServer.workspaceBuildTargets().asScala) - val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq - expect(targets.length == 2) - - val compileResult = - await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala) - expect(compileResult.getStatusCode == b.StatusCode.OK) - - } - } - } - test("diagnostics") { val inputs = TestInputs( os.rel / "Test.scala" -> diff --git a/modules/integration/src/test/scala/scala/cli/integration/JmhSuite.scala b/modules/integration/src/test/scala/scala/cli/integration/JmhSuite.scala new file mode 100644 index 0000000000..050c3c3e65 --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/JmhSuite.scala @@ -0,0 +1,29 @@ +package scala.cli.integration + +trait JmhSuite { _: ScalaCliSuite => + protected def simpleBenchmarkingInputs(directivesString: String = ""): TestInputs = TestInputs( + os.rel / "benchmark.scala" -> + s"""$directivesString + |package bench + | + |import java.util.concurrent.TimeUnit + |import org.openjdk.jmh.annotations._ + | + |@BenchmarkMode(Array(Mode.AverageTime)) + |@OutputTimeUnit(TimeUnit.NANOSECONDS) + |@Warmup(iterations = 1, time = 100, timeUnit = TimeUnit.MILLISECONDS) + |@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS) + |@Fork(0) + |class Benchmarks { + | + | @Benchmark + | def foo(): Unit = { + | (1L to 10000000L).sum + | } + | + |} + |""".stripMargin + ) + protected lazy val expectedInBenchmarkingOutput = """Result "bench.Benchmarks.foo":""" + protected lazy val exampleOldJmhVersion = "1.29" +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/JmhTests.scala b/modules/integration/src/test/scala/scala/cli/integration/JmhTests.scala index 80e92d8153..32dd7dc7ed 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/JmhTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/JmhTests.scala @@ -1,106 +1,193 @@ package scala.cli.integration +import ch.epfl.scala.bsp4j as b import com.eed3si9n.expecty.Expecty.expect import java.nio.file.Files +import scala.async.Async.{async, await} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.jdk.CollectionConverters.* import scala.util.Properties -class JmhTests extends ScalaCliSuite { - override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First - - lazy val inputs: TestInputs = TestInputs( - os.rel / "benchmark.scala" -> - s"""package bench - | - |import java.util.concurrent.TimeUnit - |import org.openjdk.jmh.annotations._ - | - |@BenchmarkMode(Array(Mode.AverageTime)) - |@OutputTimeUnit(TimeUnit.NANOSECONDS) - |@Warmup(iterations = 1, time = 100, timeUnit = TimeUnit.MILLISECONDS) - |@Measurement(iterations = 10, time = 100, timeUnit = TimeUnit.MILLISECONDS) - |@Fork(0) - |class Benchmarks { - | - | @Benchmark - | def foo(): Unit = { - | (1L to 10000000L).sum - | } - | - |} - |""".stripMargin - ) - lazy val expectedInOutput = """Result "bench.Benchmarks.foo":""" - - test("simple") { - // TODO extract running benchmarks to a separate scope, or a separate sub-command - inputs.fromRoot { root => - val res = - os.proc(TestUtil.cli, "--power", TestUtil.extraOptions, ".", "--jmh").call(cwd = root) - val output = res.out.trim() - expect(output.contains(expectedInOutput)) +class JmhTests extends ScalaCliSuite with JmhSuite with BspSuite { + override def group: ScalaCliSuite.TestGroup = ScalaCliSuite.TestGroup.First + override protected val extraOptions: Seq[String] = TestUtil.extraOptions + + for { + useDirective <- Seq(None, Some("//> using jmh")) + directiveString = useDirective.getOrElse("") + jmhOptions = if (useDirective.isEmpty) Seq("--jmh") else Nil + testMessage = useDirective match { + case None => jmhOptions.mkString(" ") + case Some(directive) => directive + } + } { + test(s"run ($testMessage)") { + // TODO extract running benchmarks to a separate scope, or a separate sub-command + simpleBenchmarkingInputs(directiveString).fromRoot { root => + val res = + os.proc(TestUtil.cli, "--power", extraOptions, ".", jmhOptions).call(cwd = root) + val output = res.out.trim() + expect(output.contains(expectedInBenchmarkingOutput)) + expect(output.contains(s"JMH version: ${Constants.jmhVersion}")) + } } - } - test("compile") { - inputs.fromRoot { root => - os.proc(TestUtil.cli, "compile", "--power", TestUtil.extraOptions, ".", "--jmh") - .call(cwd = root) + test(s"compile ($testMessage)") { + simpleBenchmarkingInputs(directiveString).fromRoot { root => + os.proc(TestUtil.cli, "compile", "--power", extraOptions, ".", jmhOptions) + .call(cwd = root) + } } - } - test("doc") { - inputs.fromRoot { root => - val res = os.proc(TestUtil.cli, "doc", "--power", TestUtil.extraOptions, ".", "--jmh") - .call(cwd = root, stderr = os.Pipe) - expect(!res.err.trim().contains("Error")) + test(s"doc ($testMessage)") { + simpleBenchmarkingInputs(directiveString).fromRoot { root => + val res = os.proc(TestUtil.cli, "doc", "--power", extraOptions, ".", jmhOptions) + .call(cwd = root, stderr = os.Pipe) + expect(!res.err.trim().contains("Error")) + } } - } - test("setup-ide") { - // TODO fix setting jmh via a reload & add tests for it - inputs.fromRoot { root => - os.proc(TestUtil.cli, "setup-ide", "--power", TestUtil.extraOptions, ".", "--jmh") - .call(cwd = root) + test(s"setup-ide ($testMessage)") { + // TODO fix setting jmh via a reload & add tests for it + simpleBenchmarkingInputs(directiveString).fromRoot { root => + os.proc(TestUtil.cli, "setup-ide", "--power", extraOptions, ".", jmhOptions) + .call(cwd = root) + } } - } - test("package") { - // TODO make package with --jmh build an artifact that actually runs benchmarks - val expectedMessage = "Placeholder main method" - inputs - .add(os.rel / "Main.scala" -> s"""@main def main: Unit = println("$expectedMessage")""") - .fromRoot { root => - val launcherName = { - val ext = if (Properties.isWin) ".bat" else "" - "launcher" + ext - } - os.proc( - TestUtil.cli, - "package", - "--power", - TestUtil.extraOptions, - ".", - "--jmh", - "-o", - launcherName + test(s"bsp ($testMessage)") { + withBsp(simpleBenchmarkingInputs(directiveString), Seq(".", "--power") ++ jmhOptions) { + (_, _, remoteServer) => + async { + val buildTargetsResp = await(remoteServer.workspaceBuildTargets().asScala) + val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq + expect(targets.length == 2) + + val compileResult = + await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala) + expect(compileResult.getStatusCode == b.StatusCode.OK) + + } + } + } + + test(s"setup-ide + bsp ($testMessage)") { + val inputs = simpleBenchmarkingInputs(directiveString) + inputs.fromRoot { root => + os.proc(TestUtil.cli, "setup-ide", "--power", extraOptions, ".", jmhOptions) + .call(cwd = root) + val ideOptionsPath = root / Constants.workspaceDirName / "ide-options-v2.json" + expect(ideOptionsPath.toNIO.toFile.exists()) + val ideLauncherOptsPath = root / Constants.workspaceDirName / "ide-launcher-options.json" + expect(ideLauncherOptsPath.toNIO.toFile.exists()) + val ideEnvsPath = root / Constants.workspaceDirName / "ide-envs.json" + expect(ideEnvsPath.toNIO.toFile.exists()) + val jsonOptions = List( + "--json-options", + ideOptionsPath.toString, + "--json-launcher-options", + ideLauncherOptsPath.toString, + "--envs-file", + ideEnvsPath.toString ) + withBsp(inputs, Seq("."), bspOptions = jsonOptions, reuseRoot = Some(root)) { + (_, _, remoteServer) => + async { + val buildTargetsResp = await(remoteServer.workspaceBuildTargets().asScala) + val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq + expect(targets.length == 2) + + val compileResult = + await(remoteServer.buildTargetCompile(new b.CompileParams(targets.asJava)).asScala) + expect(compileResult.getStatusCode == b.StatusCode.OK) + } + } + } + } + + test(s"package ($testMessage)") { + // TODO make package with --jmh build an artifact that actually runs benchmarks + val expectedMessage = "Placeholder main method" + simpleBenchmarkingInputs(directiveString) + .add(os.rel / "Main.scala" -> s"""@main def main: Unit = println("$expectedMessage")""") + .fromRoot { root => + val launcherName = { + val ext = if (Properties.isWin) ".bat" else "" + "launcher" + ext + } + os.proc( + TestUtil.cli, + "package", + "--power", + TestUtil.extraOptions, + ".", + jmhOptions, + "-o", + launcherName + ) + .call(cwd = root) + val launcher = root / launcherName + expect(os.isFile(launcher)) + expect(Files.isExecutable(launcher.toNIO)) + val output = TestUtil.maybeUseBash(launcher)(cwd = root).out.trim() + expect(output == expectedMessage) + } + } + + test(s"export ($testMessage)") { + simpleBenchmarkingInputs(directiveString).fromRoot { root => + // TODO add proper support for JMH export, we're checking if it doesn't fail the command for now + os.proc(TestUtil.cli, "export", "--power", extraOptions, ".", jmhOptions) .call(cwd = root) - val launcher = root / launcherName - expect(os.isFile(launcher)) - expect(Files.isExecutable(launcher.toNIO)) - val output = TestUtil.maybeUseBash(launcher)(cwd = root).out.trim() - expect(output == expectedMessage) } + } } - test("export") { - inputs.fromRoot { root => - // TODO add proper support for JMH export, we're checking if it doesn't fail the command for now - os.proc(TestUtil.cli, "export", "--power", TestUtil.extraOptions, ".", "--jmh") - .call(cwd = root) + for { + useDirective <- Seq(None, Some("//> using jmh false")) + directiveString = useDirective.getOrElse("") + jmhOptions = if (useDirective.isEmpty) Seq("--jmh=false") else Nil + testMessage = useDirective match { + case None => jmhOptions.mkString(" ") + case Some(directives) => directives.linesIterator.mkString("; ") + } + if !Properties.isWin + } test(s"should not compile when jmh is explicitly disabled ($testMessage)") { + simpleBenchmarkingInputs(directiveString).fromRoot { root => + val res = + os.proc(TestUtil.cli, "compile", "--power", extraOptions, ".", jmhOptions) + .call(cwd = root, check = false) + expect(res.exitCode == 1) } } + for { + useDirective <- Seq( + None, + Some( + s"""//> using jmh + |//> using jmhVersion $exampleOldJmhVersion + |""".stripMargin + ) + ) + directiveString = useDirective.getOrElse("") + jmhOptions = + if (useDirective.isEmpty) Seq("--jmh", "--jmh-version", exampleOldJmhVersion) else Nil + testMessage = useDirective match { + case None => jmhOptions.mkString(" ") + case Some(directives) => directives.linesIterator.mkString("; ") + } + if !Properties.isWin + } test(s"should use the passed jmh version ($testMessage)") { + simpleBenchmarkingInputs(directiveString).fromRoot { root => + val res = + os.proc(TestUtil.cli, "run", "--power", extraOptions, ".", jmhOptions) + .call(cwd = root) + val output = res.out.trim() + expect(output.contains(expectedInBenchmarkingOutput)) + expect(output.contains(s"JMH version: $exampleOldJmhVersion")) + } + } } diff --git a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala index 695022ec04..193c01331e 100644 --- a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala @@ -459,7 +459,7 @@ final case class BuildOptions( fetchSources = classPathOptions.fetchSources.getOrElse(false), addJvmRunner = addRunnerDependency0, addJvmTestRunner = isTests && addJvmTestRunner, - addJmhDependencies = jmhOptions.addJmhDependencies, + addJmhDependencies = jmhOptions.finalJmhVersion, extraRepositories = value(finalRepositories), keepResolution = internal.keepResolution, includeBuildServerDeps = useBuildServer.getOrElse(true), diff --git a/modules/options/src/main/scala/scala/build/options/JmhOptions.scala b/modules/options/src/main/scala/scala/build/options/JmhOptions.scala index 63132807e8..084a8bb082 100644 --- a/modules/options/src/main/scala/scala/build/options/JmhOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/JmhOptions.scala @@ -1,9 +1,29 @@ package scala.build.options +import scala.build.internal.Constants + +/** @param jmhVersion + * the version of JMH to be used in the build + * @param enableJmh + * toggle for enabling JMH dependency handling in the build (overrides [[runJmh]] when disabled) + * @param runJmh + * toggle for whether JMH should actually be runnable from this build (this value gets changed in + * JMH builds to detect which main class is to be called as benchmarks are being run) + */ final case class JmhOptions( - addJmhDependencies: Option[String] = None, + jmhVersion: Option[String] = None, + enableJmh: Option[Boolean] = None, runJmh: Option[Boolean] = None -) +) { + def finalJmhVersion: Option[String] = jmhVersion.orElse(enableJmh match { + case Some(true) => Some(Constants.jmhVersion) + case _ => None + }) + + def canRunJmh: Boolean = + (for { enabled <- enableJmh; runnable <- runJmh } yield enabled && runnable) + .getOrElse(false) +} object JmhOptions { implicit val hasHashData: HasHashData[JmhOptions] = HasHashData.derive diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index f9ab473e92..ab48824a2f 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -56,7 +56,7 @@ Run JMH benchmarks ### `--jmh-version` -Set JMH version +Set JMH version (default: 1.37) ## Compilation server options diff --git a/website/docs/reference/directives.md b/website/docs/reference/directives.md index c02794d6f6..6dea14e956 100644 --- a/website/docs/reference/directives.md +++ b/website/docs/reference/directives.md @@ -5,6 +5,17 @@ sidebar_position: 2 ## using directives +### Benchmarking options + +Add benchmarking options + +`//> using jmh` _value_ + +`//> using jmhVersion` _value_ + +#### Examples +`//> using jmhVersion 1.37` + ### BuildInfo Generate BuildInfo for project