From faf6348a16de053903a7aeed0c2491e1fb7be2be Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 13 Feb 2019 11:36:13 -0800 Subject: [PATCH] Run gc when idle I often find that when I run a command it takes a long time to start up because sbt triggers a full gc. To improve the ux, I update the command exchange to run full gc only once while it's waiting for a command to run and only after the user has been idle for at least one minute. Bonus: optimize imports --- main/src/main/scala/sbt/Main.scala | 6 +- .../scala/sbt/internal/CommandExchange.scala | 58 ++++++++++++------- main/src/main/scala/sbt/internal/GCUtil.scala | 2 +- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index c5e053c91..275ed4d3d 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -884,7 +884,11 @@ object BuiltinCommands { val exchange = StandardMain.exchange val s1 = exchange run s0 exchange publishEventMessage ConsolePromptEvent(s0) - val exec: Exec = exchange.blockUntilNextExec + val minGCInterval = Project + .extract(s1) + .getOpt(Keys.minForcegcInterval) + .getOrElse(GCUtil.defaultMinForcegcInterval) + val exec: Exec = exchange.blockUntilNextExec(minGCInterval, s1.globalLogging.full) val newState = s1 .copy( onFailure = Some(Exec(Shell, None)), diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index a2e1aa77c..dd08f56f4 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -11,32 +11,36 @@ package internal import java.io.IOException import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic._ + import scala.collection.mutable.ListBuffer import scala.annotation.tailrec import BasicKeys.{ autoStartServer, - serverHost, - serverPort, + fullServerHandlers, + logLevel, serverAuthentication, serverConnectionType, + serverHost, serverLogLevel, - fullServerHandlers, - logLevel + serverPort } import java.net.Socket + +import sbt.Watched.NullLogger import sjsonnew.JsonFormat import sjsonnew.shaded.scalajson.ast.unsafe._ + import scala.concurrent.Await -import scala.concurrent.duration.Duration -import scala.util.{ Success, Failure, Try } +import scala.concurrent.duration._ +import scala.util.{ Failure, Success, Try } import sbt.io.syntax._ import sbt.io.{ Hash, IO } import sbt.internal.server._ import sbt.internal.langserver.{ LogMessageParams, MessageType } -import sbt.internal.util.{ StringEvent, ObjectEvent, MainAppender } +import sbt.internal.util.{ MainAppender, ObjectEvent, StringEvent } import sbt.internal.util.codec.JValueFormats import sbt.protocol.{ EventMessage, ExecStatusEvent } -import sbt.util.{ Level, Logger, LogExchange } +import sbt.util.{ Level, LogExchange, Logger } /** * The command exchange merges multiple command channels (e.g. network and console), @@ -59,22 +63,34 @@ private[sbt] final class CommandExchange { def channels: List[CommandChannel] = channelBuffer.toList def subscribe(c: CommandChannel): Unit = channelBufferLock.synchronized(channelBuffer.append(c)) + def blockUntilNextExec: Exec = blockUntilNextExec(Duration.Inf, NullLogger) // periodically move all messages from all the channels - @tailrec def blockUntilNextExec: Exec = { - @tailrec def slurpMessages(): Unit = - channels.foldLeft(Option.empty[Exec]) { _ orElse _.poll } match { - case None => () - case Some(x) => - commandQueue.add(x) - slurpMessages + private[sbt] def blockUntilNextExec(interval: Duration, logger: Logger): Exec = { + @tailrec def impl(deadline: Option[Deadline]): Exec = { + @tailrec def slurpMessages(): Unit = + channels.foldLeft(Option.empty[Exec]) { _ orElse _.poll } match { + case None => () + case Some(x) => + commandQueue.add(x) + slurpMessages + } + slurpMessages() + Option(commandQueue.poll) match { + case Some(x) => x + case None => + Thread.sleep(50) + val newDeadline = if (deadline.fold(false)(_.isOverdue())) { + GCUtil.forceGcWithInterval(interval, logger) + None + } else deadline + impl(newDeadline) } - slurpMessages() - Option(commandQueue.poll) match { - case Some(x) => x - case None => - Thread.sleep(50) - blockUntilNextExec } + // Do not manually run GC until the user has been idling for at least the min gc interval. + impl(interval match { + case d: FiniteDuration => Some(d.fromNow) + case _ => None + }) } def run(s: State): State = { diff --git a/main/src/main/scala/sbt/internal/GCUtil.scala b/main/src/main/scala/sbt/internal/GCUtil.scala index b74d2bf03..2e75209d1 100644 --- a/main/src/main/scala/sbt/internal/GCUtil.scala +++ b/main/src/main/scala/sbt/internal/GCUtil.scala @@ -17,7 +17,7 @@ private[sbt] object GCUtil { // Returns the default force garbage collection flag, // as specified by system properties. val defaultForceGarbageCollection: Boolean = true - val defaultMinForcegcInterval: Duration = 60.seconds + val defaultMinForcegcInterval: Duration = 10.minutes val lastGcCheck: AtomicLong = new AtomicLong(0L) def forceGcWithInterval(minForcegcInterval: Duration, log: Logger): Unit = {