Merge pull request #1534 from havocp/wip/havocp-server-mem

Set JVM memory options for server applications
This commit is contained in:
eugene yokota 2014-08-16 17:10:47 -04:00
commit ad34bf77e7
2 changed files with 107 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,74 @@ object ServerLauncher {
finally reader.close()
}
// None = couldn't figure it out
def javaIsAbove(currentDirectory: File, version: Int): Option[Boolean] = try {
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 pattern = java.util.regex.Pattern.compile("""java version "[0-9]+\.([0-9]+)\..*".*""")
lineOption flatMap { line =>
val matcher = pattern.matcher(line)
if (matcher.matches()) {
try Some(Integer.parseInt(matcher.group(1)) > version) catch { case NonFatal(_) => None }
} else {
System.err.println(s"Failed to parse version from 'java -version' output '$line'")
None
}
}
} finally {
process.destroy()
try { process.waitFor() } catch { case NonFatal(_) => }
}
} catch {
case e: IOException =>
// both process.start and reading the output streams can throw IOException.
// all OS exceptions from process.start are supposed to be IOException.
System.err.println(s"Failed to run 'java -version': ${e.getClass.getName}: ${e.getMessage}")
None
}
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"))
}
}
}
}
}