From faf6348a16de053903a7aeed0c2491e1fb7be2be Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 13 Feb 2019 11:36:13 -0800 Subject: [PATCH 1/3] 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 = { From c37f2607f17bd942ebd9c12b3bd8dd45f4fa1b52 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Thu, 14 Feb 2019 21:09:24 -0800 Subject: [PATCH 2/3] Lint CommandExchange.scala Clears out intellij warnings. --- main/src/main/scala/sbt/internal/CommandExchange.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index dd08f56f4..70c4a21e8 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -72,7 +72,7 @@ private[sbt] final class CommandExchange { case None => () case Some(x) => commandQueue.add(x) - slurpMessages + slurpMessages() } slurpMessages() Option(commandQueue.poll) match { @@ -99,7 +99,7 @@ private[sbt] final class CommandExchange { consoleChannel = Some(console0) subscribe(console0) } - val autoStartServerAttr = (s get autoStartServer) match { + val autoStartServerAttr = s get autoStartServer match { case Some(bool) => bool case None => true } @@ -277,13 +277,13 @@ private[sbt] final class CommandExchange { val toDel: ListBuffer[CommandChannel] = ListBuffer.empty def json: JValue = JObject( JField("type", JString(event.contentType)), - (Vector(JField("message", event.json), JField("level", JString(event.level.toString))) ++ + Vector(JField("message", event.json), JField("level", JString(event.level.toString))) ++ (event.channelName.toVector map { channelName => JField("channelName", JString(channelName)) }) ++ (event.execId.toVector map { execId => JField("execId", JString(execId)) - })): _* + }): _* ) channels collect { case c: ConsoleChannel => From fcd24ba7cd7b26c04d750ec70b026e5d5d654356 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Thu, 14 Feb 2019 21:12:12 -0800 Subject: [PATCH 3/3] Reduce command polling period The only thing this function does is poll a queue and check a deadline. There is no need to put such a large sleep duration in. --- main/src/main/scala/sbt/internal/CommandExchange.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 70c4a21e8..5f601a74e 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -78,7 +78,7 @@ private[sbt] final class CommandExchange { Option(commandQueue.poll) match { case Some(x) => x case None => - Thread.sleep(50) + Thread.sleep(2) val newDeadline = if (deadline.fold(false)(_.isOverdue())) { GCUtil.forceGcWithInterval(interval, logger) None