diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 7091067ed..7d1145b21 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -157,7 +157,7 @@ object Defaults extends BuildCommon { fork :== false, initialize :== {}, forcegc :== sys.props.get("sbt.task.forcegc").map(java.lang.Boolean.parseBoolean).getOrElse(EvaluateTaskConfig.defaultForceGarbageCollection), - maxObjectPendingFinalization :== sys.props.get("sbt.task.maxObjectPendingFinalization").map(java.lang.Integer.parseInt).getOrElse(EvaluateTaskConfig.defaultMaxObjectPendingFinalization) + minForcegcInterval :== sys.props.get("sbt.task.minForcegcInterval").map(java.lang.Integer.parseInt).getOrElse(EvaluateTaskConfig.defaultMinForcegcInterval) )) def defaultTestTasks(key: Scoped): Seq[Setting[_]] = inTask(key)(Seq( tags := Seq(Tags.Test -> 1), diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index ffe39dc85..ba0e9bd03 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -4,7 +4,6 @@ package sbt import java.io.File -import java.lang.management.{ ManagementFactory, MemoryMXBean } import Def.{ displayFull, dummyState, ScopedKey, Setting } import Keys.{ streams, Streams, TaskStreams } import Keys.{ dummyRoots, dummyStreamsManager, executionRoots, pluginData, streamsManager, taskDefinitionKey, transformState } @@ -57,6 +56,7 @@ object TaskCancellationStrategy { type State = Unit def onTaskEngineStart(canceller: RunningTaskEngine): Unit = () def onTaskEngineFinish(state: Unit): Unit = () + override def toString: String = "Null" } /** Cancel handler which registers for SIGINT and cancels tasks when it is received. */ object Signal extends TaskCancellationStrategy { @@ -66,6 +66,7 @@ object TaskCancellationStrategy { } def onTaskEngineFinish(registration: Signals.Registration): Unit = registration.remove() + override def toString: String = "Signal" } } @@ -87,15 +88,15 @@ sealed trait EvaluateTaskConfig { def forceGarbageCollection: Boolean /** - * Threshold to forcing garbage collection. + * Interval in seconds. */ - def maxObjectPendingFinalization: Int + def minForcegcInterval: Int } final object EvaluateTaskConfig { // Returns the default force garbage collection flag, // as specified by system properties. private[sbt] def defaultForceGarbageCollection: Boolean = true - private[sbt] def defaultMaxObjectPendingFinalization: Int = 1024 + private[sbt] def defaultMinForcegcInterval: Int = 60 /** Pulls in the old configuration format. */ def apply(old: EvaluateConfig): EvaluateTaskConfig = { @@ -107,39 +108,40 @@ final object EvaluateTaskConfig { if (old.cancelable) TaskCancellationStrategy.Signal else TaskCancellationStrategy.Null def forceGarbageCollection = defaultForceGarbageCollection - def maxObjectPendingFinalization = defaultMaxObjectPendingFinalization + def minForcegcInterval = defaultMinForcegcInterval } AdaptedTaskConfig } - @deprecated("Use the alternative that specifies maxObjectPendingFinalization", "0.13.7") + @deprecated("Use the alternative that specifies minForcegcInterval", "0.13.9") def apply(restrictions: Seq[Tags.Rule], checkCycles: Boolean, progressReporter: ExecuteProgress[Task], cancelStrategy: TaskCancellationStrategy, forceGarbageCollection: Boolean): EvaluateTaskConfig = apply(restrictions, checkCycles, progressReporter, cancelStrategy, forceGarbageCollection, - defaultMaxObjectPendingFinalization) + defaultMinForcegcInterval) + /** Raw constructor for EvaluateTaskConfig. */ def apply(restrictions: Seq[Tags.Rule], checkCycles: Boolean, progressReporter: ExecuteProgress[Task], cancelStrategy: TaskCancellationStrategy, forceGarbageCollection: Boolean, - maxObjectPendingFinalization: Int): EvaluateTaskConfig = { + minForcegcInterval: Int): EvaluateTaskConfig = { val r = restrictions val check = checkCycles val cs = cancelStrategy val pr = progressReporter val fgc = forceGarbageCollection - val mopf = maxObjectPendingFinalization + val mfi = minForcegcInterval object SimpleEvaluateTaskConfig extends EvaluateTaskConfig { def restrictions = r def checkCycles = check def progressReporter = pr def cancelStrategy = cs def forceGarbageCollection = fgc - def maxObjectPendingFinalization = mopf + def minForcegcInterval = mfi } SimpleEvaluateTaskConfig } @@ -166,7 +168,6 @@ object EvaluateTask { if (java.lang.Boolean.getBoolean("sbt.task.timings")) new TaskTimings else ExecuteProgress.empty[Task] val SystemProcessors = Runtime.getRuntime.availableProcessors - lazy val memoryMxBean: MemoryMXBean = ManagementFactory.getMemoryMXBean @deprecated("Use extractedTaskConfig.", "0.13.0") def defaultConfig(state: State): EvaluateConfig = @@ -200,8 +201,8 @@ object EvaluateTask { val canceller = cancelStrategy(extracted, structure, state) val progress = executeProgress(extracted, structure, state) val fgc = forcegc(extracted, structure) - val mopf = maxObjectPendingFinalization(extracted, structure) - EvaluateTaskConfig(rs, false, progress, canceller, fgc, mopf) + val mfi = minForcegcInterval(extracted, structure) + EvaluateTaskConfig(rs, false, progress, canceller, fgc, mfi) } def defaultRestrictions(maxWorkers: Int) = Tags.limitAll(maxWorkers) :: Nil @@ -234,8 +235,8 @@ object EvaluateTask { private[sbt] def forcegc(extracted: Extracted, structure: BuildStructure): Boolean = getSetting(Keys.forcegc in Global, EvaluateTaskConfig.defaultForceGarbageCollection, extracted, structure) // TODO - Should this pull from Global or from the project itself? - private[sbt] def maxObjectPendingFinalization(extracted: Extracted, structure: BuildStructure): Int = - getSetting(Keys.maxObjectPendingFinalization in Global, EvaluateTaskConfig.defaultMaxObjectPendingFinalization, extracted, structure) + private[sbt] def minForcegcInterval(extracted: Extracted, structure: BuildStructure): Int = + getSetting(Keys.minForcegcInterval in Global, EvaluateTaskConfig.defaultMinForcegcInterval, extracted, structure) def getSetting[T](key: SettingKey[T], default: T, extracted: Extracted, structure: BuildStructure): T = key in extracted.currentRef get structure.data getOrElse default @@ -337,29 +338,17 @@ object EvaluateTask { import ConcurrentRestrictions.{ completionService, TagMap, Tag, tagged, tagsKey } val log = state.log - log.debug("Running task... Cancel: " + config.cancelStrategy + ", check cycles: " + config.checkCycles) + log.debug(s"Running task... Cancel: ${config.cancelStrategy}, check cycles: ${config.checkCycles}, forcegc: ${config.forceGarbageCollection}") val tags = tagged[Task[_]](_.info get tagsKey getOrElse Map.empty, Tags.predicate(config.restrictions)) val (service, shutdownThreads) = completionService[Task[_], Completed](tags, (s: String) => log.warn(s)) def shutdown(): Unit = { // First ensure that all threads are stopped for task execution. shutdownThreads() + // Now we run the gc cleanup to force finalizers to clear out file handles (yay GC!) if (config.forceGarbageCollection) { - try { - val opfc = memoryMxBean.getObjectPendingFinalizationCount - if (opfc > config.maxObjectPendingFinalization) { - log.info(s"Forcing garbage collection... Objects pending finalization: $opfc") - // Force the detection of finalizers for scala.reflect weakhashsets - System.gc() - // Force finalizers to run. - System.runFinalization() - // Force actually cleaning the weak hash maps. - System.gc() - } - } catch { - case _: Throwable => // gotta catch em all - } + GCUtil.forceGcWithInterval(config.minForcegcInterval, log) } } // propagate the defining key for reporting the origin diff --git a/main/src/main/scala/sbt/GCUtil.scala b/main/src/main/scala/sbt/GCUtil.scala new file mode 100644 index 000000000..5f0bfc4e7 --- /dev/null +++ b/main/src/main/scala/sbt/GCUtil.scala @@ -0,0 +1,32 @@ +package sbt + +import java.util.concurrent.atomic.AtomicLong +import scala.util.control.NonFatal + +private[sbt] object GCUtil { + val lastGcCheck: AtomicLong = new AtomicLong(0L) + + def forceGcWithInterval(minForcegcInterval: Int, log: Logger): Unit = + { + val now = System.currentTimeMillis + val last = lastGcCheck.get + // This throttles System.gc calls to interval + if (now - last > minForcegcInterval * 1000) { + lastGcCheck.set(now) + forceGc(log) + } + } + + def forceGc(log: Logger): Unit = + try { + log.debug(s"Forcing garbage collection...") + // Force the detection of finalizers for scala.reflect weakhashsets + System.gc() + // Force finalizers to run. + System.runFinalization() + // Force actually cleaning the weak hash maps. + System.gc() + } catch { + case NonFatal(_) => // gotta catch em all + } +} diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index a9163547c..6cf2cd3bb 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -342,7 +342,7 @@ object Keys { val concurrentRestrictions = SettingKey[Seq[Tags.Rule]]("concurrent-restrictions", "Rules describing restrictions on concurrent task execution.", BSetting) val cancelable = SettingKey[Boolean]("cancelable", "Enables (true) or disables (false) the ability to interrupt task execution with CTRL+C.", BMinusSetting) val forcegc = SettingKey[Boolean]("forcegc", "Enables (true) or disables (false) forcing garbage collection after task run when needed.", BMinusSetting) - val maxObjectPendingFinalization = SettingKey[Int]("max-object-pending-finalization", "Threshold to forcing garbage collection.") + val minForcegcInterval = SettingKey[Int]("min-forcegc-interval", "Minimal interval (in seconds) to check for forcing garbage collection.") val settingsData = std.FullInstance.settingsData val streams = TaskKey[TaskStreams]("streams", "Provides streams for logging and persisting data.", DTask) val taskDefinitionKey = Def.taskDefinitionKey