Set Java memory options for server applications

The defaults here are copied from Activator which
in turn are probably copied from somewhere else.
They are of course basically arbitrary, but the
JVM's defaults if we don't set memory options
are insufficient to run most apps.

If the user sets any memory options in the configuration,
we completely leave them alone and don't set any of
our own. So it's always possible to override.
This commit is contained in:
Havoc Pennington 2014-08-15 11:07:19 -04:00
parent b0db0a847e
commit 3a065dc047
2 changed files with 96 additions and 5 deletions

View File

@ -136,10 +136,7 @@ object ServerLauncher {
if (System.getenv("SBT_SERVER_SAVE_TEMPS") eq null)
launchConfig.deleteOnExit()
LaunchConfiguration.save(config, launchConfig)
val jvmArgs: List[String] = serverConfig.jvmArgs map readLines match {
case Some(args) => args
case None => Nil
}
val jvmArgs: List[String] = serverJvmArgs(currentDirectory, serverConfig)
val cmd: List[String] =
("java" :: jvmArgs) ++
("-jar" :: defaultLauncherLookup.getCanonicalPath :: s"@load:${launchConfig.toURI.toURL.toString}" :: Nil)
@ -211,6 +208,63 @@ object ServerLauncher {
finally reader.close()
}
// None = couldn't figure it out
def javaIsAbove(currentDirectory: File, version: Int): Option[Boolean] = {
val pb = new java.lang.ProcessBuilder()
// hopefully "java -version" is a lot faster than booting the full JVM.
// not sure how else we can do this.
pb.command("java", "-version")
pb.directory(currentDirectory)
val process = pb.start()
try {
process.getOutputStream.close()
process.getInputStream.close()
val stderr = new java.io.LineNumberReader(new java.io.InputStreamReader(process.getErrorStream))
// Looking for the first line which is `java version "1.7.0_60"` or similar
val lineOption = try Option(stderr.readLine()) finally stderr.close()
val re = """java version "[0-9]+\.([0-9]+)\..*".*""".r
lineOption flatMap {
case re(v) => try Some(Integer.parseInt(v) > version) catch { case NonFatal(_) => None }
case other => None
}
} finally {
process.destroy()
try { process.waitFor() } catch { case NonFatal(_) => }
}
}
def serverJvmArgs(currentDirectory: File, serverConfig: ServerConfiguration): List[String] =
serverJvmArgs(currentDirectory, serverConfig.jvmArgs map readLines getOrElse Nil)
final val memOptPrefixes = List("-Xmx", "-Xms", "-XX:MaxPermSize", "-XX:PermSize", "-XX:ReservedCodeCacheSize", "-XX:MaxMetaspaceSize", "-XX:MetaspaceSize")
final val defaultMinHeapM = 256
final val defaultMaxHeapM = defaultMinHeapM * 4
final val defaultMinPermM = 64
final val defaultMaxPermM = defaultMinPermM * 4
// this is separate just for the test suite
def serverJvmArgs(currentDirectory: File, baseArgs: List[String]): List[String] = {
// ignore blank lines
val trimmed = baseArgs.map(_.trim).filterNot(_.isEmpty)
// If the user config has provided ANY memory options we bail out and do NOT add
// any defaults. This means people can always fix our mistakes, and it avoids
// issues where the JVM refuses to start because of (for example) min size greater
// than max size. We don't want to deal with coordinating our changes with the
// user configuration.
def isMemoryOption(s: String) = memOptPrefixes.exists(s.startsWith(_))
if (trimmed.exists(isMemoryOption(_)))
trimmed
else {
val permOptions = javaIsAbove(currentDirectory, 7) match {
case Some(true) => List(s"-XX:MetaspaceSize=${defaultMinPermM}m", s"-XX:MaxMetaspaceSize=${defaultMaxPermM}m")
case Some(false) => List(s"-XX:PermSize=${defaultMinPermM}m", s"-XX:MaxPermSize=${defaultMaxPermM}m")
case None => Nil // don't know what we're doing, so don't set options
}
s"-Xms${defaultMinHeapM}m" :: s"-Xmx${defaultMaxHeapM}m" :: (permOptions ++ trimmed)
}
}
def defaultLauncherLookup: File =
try {
val classInLauncher = classOf[AppConfiguration]

View File

@ -48,5 +48,42 @@ object ServerLocatorTest extends Specification {
finally inputStream.close()
result must equalTo(Some(expected))
}
"determine a JVM version" in {
withTemporaryDirectory { dir =>
// javaIs8OrAbove returns None for pathological situations
// (weird errors running java -version or something),
// but when testing sbt we should not be in such a situation.
val determined = ServerLauncher.javaIsAbove(dir, 7)
determined must beSome
}
}
"have JVM memory defaults" in {
withTemporaryDirectory { dir =>
val defaults = ServerLauncher.serverJvmArgs(dir, Nil)
defaults must contain(beEqualTo("-Xms256m"))
defaults must contain(beEqualTo("-Xmx1024m"))
if (ServerLauncher.javaIsAbove(dir, 7).getOrElse(false)) {
defaults must contain(beEqualTo("-XX:MetaspaceSize=64m"))
defaults must contain(beEqualTo("-XX:MaxMetaspaceSize=256m"))
} else {
defaults must contain(beEqualTo("-XX:PermSize=64m"))
defaults must contain(beEqualTo("-XX:MaxPermSize=256m"))
}
}
}
"leave user-specified memory options alone" in {
withTemporaryDirectory { dir =>
val args = ServerLauncher.serverJvmArgs(dir, List("-Xmx4321m"))
args must contain(beEqualTo("-Xmx4321m"))
args must not contain (beEqualTo("-Xms256m"))
args must not contain (beEqualTo("-Xmx1024m"))
}
}
"ignore whitespace in jvm args file" in {
withTemporaryDirectory { dir =>
val args = ServerLauncher.serverJvmArgs(dir, List("", " ", " -Xmx4321m ", " ", ""))
args must equalTo(List("-Xmx4321m"))
}
}
}
}
}