From d58aab5d8471c4ed75002fbab79a46d1fce075e0 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Sun, 9 Aug 2020 17:39:15 -0700 Subject: [PATCH] Add super shell options This commit adds a few options to supershell: 1. Max items -- sets the max number of tasks to display in the progress reports. It is pretty hard to read more than a few items in the progress reports so I set the default limit to 8 and made that configurable via the superShellMaxTasks parameter. If there are more than the limit, there is an additional line telling how many additional tasks are running 2. sleep -- sets how long to sleep between reports. The default is 500ms to ensure that it updates at least once per second but the previous value of 100ms is more frequent than necessary 3. threshold -- sets the minimum duration a task has to run before being printed in the progress reports. The default threshold is increased from 10ms to 100ms. This introduces a delay of threshold milliseconds before any progress lines appear and also means that if no tasks ever exceed the threshold, then no progress is ever displayed. --- .../sbt/internal/util/ProgressState.scala | 24 ++++++++++++------- .../scala/sbt/internal/util/Terminal.scala | 8 ++++++- main/src/main/scala/sbt/Defaults.scala | 8 +++++-- main/src/main/scala/sbt/Keys.scala | 4 ++++ main/src/main/scala/sbt/Main.scala | 14 ++++++++++- main/src/main/scala/sbt/MainLoop.scala | 8 ++++++- .../scala/sbt/internal/CommandExchange.scala | 3 ++- .../src/main/scala/sbt/internal/SysProp.scala | 4 +++- .../scala/sbt/internal/TaskProgress.scala | 4 +--- .../sbt/internal/server/NetworkChannel.scala | 11 +++++++-- 10 files changed, 67 insertions(+), 21 deletions(-) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala index 3b3b4d125..d6b594578 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala @@ -26,14 +26,16 @@ private[sbt] final class ProgressState( val padding: AtomicInteger, val blankZone: Int, val currentLineBytes: AtomicReference[ArrayBuffer[Byte]], + val maxItems: Int, ) { - def this(blankZone: Int) = - this( - new AtomicReference(Nil), - new AtomicInteger(0), - blankZone, - new AtomicReference(new ArrayBuffer[Byte]), - ) + def this(blankZone: Int, maxItems: Int) = this( + new AtomicReference(Nil), + new AtomicInteger(0), + blankZone, + new AtomicReference(new ArrayBuffer[Byte]), + maxItems, + ) + def this(blankZone: Int) = this(blankZone, 8) def currentLine: Option[String] = new String(currentLineBytes.get.toArray, "UTF-8").linesIterator.toSeq.lastOption .map(EscHelpers.stripColorsAndMoves) @@ -162,14 +164,18 @@ private[sbt] object ProgressState { terminal.withPrintStream { ps => val commandFromThisTerminal = pe.channelName.fold(true)(_ == terminal.name) val info = if (commandFromThisTerminal) { - pe.items.map { item => + val base = pe.items.map { item => val elapsed = item.elapsedMicros / 1000000L s" | => ${item.name} ${elapsed}s" } + val limit = state.maxItems + if (base.size > limit) + s" | ... (${base.size - limit} other tasks)" +: base.takeRight(limit) + else base } else { pe.command.toSeq.flatMap { cmd => val width = terminal.getWidth - val sanitized = if ((cmd.length + SERVER_IS_RUNNING_LENGTH) < width) { + val sanitized = if ((cmd.length + SERVER_IS_RUNNING_LENGTH) > width) { if (SERVER_IS_RUNNING_LENGTH + cmd.length < width) cmd else cmd.take(MIN_COMMAND_WIDTH) + "..." } else cmd diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala index 1137829d3..3c7d7a008 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala @@ -141,7 +141,7 @@ trait Terminal extends AutoCloseable { private[sbt] def withPrintStream[T](f: PrintStream => T): T private[sbt] def withRawOutput[R](f: => R): R private[sbt] def restore(): Unit = {} - private[sbt] val progressState = new ProgressState(1) + private[sbt] def progressState: ProgressState private[this] val promptHolder: AtomicReference[Prompt] = new AtomicReference(Prompt.Pending) private[sbt] final def prompt: Prompt = promptHolder.get private[sbt] final def setPrompt(newPrompt: Prompt): Unit = @@ -315,6 +315,7 @@ object Terminal { private[this] object ProxyTerminal extends Terminal { private def t: Terminal = activeTerminal.get + override private[sbt] def progressState: ProgressState = t.progressState override def getWidth: Int = t.getWidth override def getHeight: Int = t.getHeight override def getLineHeightAndWidth(line: String): (Int, Int) = t.getLineHeightAndWidth(line) @@ -755,6 +756,9 @@ object Terminal { private val capabilityMap = org.jline.utils.InfoCmp.Capability.values().map(c => c.toString -> c).toMap + private val consoleProgressState = new AtomicReference[ProgressState](new ProgressState(1)) + private[sbt] def setConsoleProgressState(progressState: ProgressState): Unit = + consoleProgressState.set(progressState) @deprecated("For compatibility only", "1.4.0") private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine @@ -770,6 +774,7 @@ object Terminal { } private[this] val isCI = sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI") override lazy val isAnsiSupported: Boolean = term.isAnsiSupported && !isCI + override private[sbt] def progressState: ProgressState = consoleProgressState.get override def isEchoEnabled: Boolean = system.echo() override def isSuccessEnabled: Boolean = true override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = @@ -912,6 +917,7 @@ object Terminal { new WriteableInputStream(nullInputStream, "null-writeable-input-stream") private[sbt] val NullTerminal = new Terminal { override def close(): Unit = {} + override private[sbt] def progressState: ProgressState = new ProgressState(1) override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = false override def getHeight: Int = 0 override def getLastLine: Option[String] = None diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 7fbf1aec9..2f9a4498b 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -97,7 +97,7 @@ import sjsonnew._ import sjsonnew.support.scalajson.unsafe.Converter import scala.collection.immutable.ListMap -import scala.concurrent.duration.FiniteDuration +import scala.concurrent.duration._ import scala.util.control.NonFatal import scala.xml.NodeSeq @@ -393,11 +393,15 @@ object Defaults extends BuildCommon { else appConfiguration.value.provider.scalaProvider.launcher.topLoader.getParent }, useSuperShell := { if (insideCI.value) false else Terminal.console.isSupershellEnabled }, + superShellThreshold :== SysProp.supershellThreshold, + superShellMaxTasks :== SysProp.supershellMaxTasks, + superShellSleep :== SysProp.supershellSleep.millis, progressReports := { val rs = EvaluateTask.taskTimingProgress.toVector ++ EvaluateTask.taskTraceEvent.toVector rs map { Keys.TaskProgress(_) } }, - progressState := Some(new ProgressState(SysProp.supershellBlankZone)), + // progressState is deprecated + SettingKey[Option[ProgressState]]("progressState") := None, Previous.cache := new Previous( Def.streamsManagerKey.value, Previous.references.value.getReferences diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index bdc25000c..30cfe2425 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -557,9 +557,13 @@ object Keys { private[sbt] val currentTaskProgress = AttributeKey[TaskProgress]("current-task-progress") private[sbt] val taskProgress = AttributeKey[sbt.internal.TaskProgress]("active-task-progress") val useSuperShell = settingKey[Boolean]("Enables (true) or disables the super shell.") + val superShellMaxTasks = settingKey[Int]("The max number of tasks to display in the supershell progress report") + val superShellSleep = settingKey[FiniteDuration]("The minimum duration to sleep between progress reports") + val superShellThreshold = settingKey[FiniteDuration]("The minimum amount of time a task must be running to appear in the supershell progress report") val turbo = settingKey[Boolean]("Enables (true) or disables optional performance features.") // This key can be used to add custom ExecuteProgress instances val progressReports = settingKey[Seq[TaskProgress]]("A function that returns a list of progress reporters.").withRank(DTask) + @deprecated("unused", "1.4.0") private[sbt] val progressState = settingKey[Option[ProgressState]]("The optional progress state if supershell is enabled.").withRank(Invisible) private[sbt] val postProgressReports = settingKey[Unit]("Internally used to modify logger.").withRank(DTask) @deprecated("No longer used", "1.3.0") diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 8361d1492..34e6333c6 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -935,13 +935,25 @@ object BuiltinCommands { // This is a workaround for the console task in dotty which uses the classloader cache. // We need to override the top loader in that case so that it gets the forked jline. s5.extendedClassLoaderCache.setParent(Project.extract(s5).get(Keys.scalaInstanceTopLoader)) - CheckBuildSources.init(LintUnused.lintUnusedFunc(s5)) + addSuperShellParams(CheckBuildSources.init(LintUnused.lintUnusedFunc(s5))) } private val setupGlobalFileTreeRepository: State => State = { state => state.get(sbt.nio.Keys.globalFileTreeRepository).foreach(_.close()) state.put(sbt.nio.Keys.globalFileTreeRepository, FileTreeRepository.default) } + private val addSuperShellParams: State => State = (s: State) => { + val extracted = Project.extract(s) + import scala.concurrent.duration._ + val sleep = extracted.getOpt(Keys.superShellSleep).getOrElse(SysProp.supershellSleep.millis) + val threshold = + extracted.getOpt(Keys.superShellThreshold).getOrElse(SysProp.supershellThreshold) + val maxItems = extracted.getOpt(Keys.superShellMaxTasks).getOrElse(SysProp.supershellMaxTasks) + Terminal.setConsoleProgressState(new ProgressState(1, maxItems)) + s.put(Keys.superShellSleep.key, sleep) + .put(Keys.superShellThreshold.key, threshold) + .put(Keys.superShellMaxTasks.key, maxItems) + } private val addCacheStoreFactoryFactory: State => State = (s: State) => { val size = Project .extract(s) diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index 801465b8d..7bcea26ce 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -22,8 +22,10 @@ import sbt.protocol._ import sbt.util.{ Logger, LoggerContext } import scala.annotation.tailrec +import scala.concurrent.duration._ import scala.util.control.NonFatal import sbt.internal.FastTrackCommands +import sbt.internal.SysProp object MainLoop { @@ -150,7 +152,11 @@ object MainLoop { def next(state: State): State = { val context = LoggerContext(useLog4J = state.get(Keys.useLog4J.key).getOrElse(false)) - val taskProgress = new TaskProgress + val superShellSleep = + state.get(Keys.superShellSleep.key).getOrElse(SysProp.supershellSleep.millis) + val superShellThreshold = + state.get(Keys.superShellThreshold.key).getOrElse(SysProp.supershellThreshold) + val taskProgress = new TaskProgress(superShellSleep, superShellThreshold) try { ErrorHandling.wideConvert { state diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 6cb4f4ece..505c913c2 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -201,7 +201,8 @@ private[sbt] final class CommandExchange { instance, handlers, s.log, - mkAskUser(name) + mkAskUser(name), + Option(lastState.get), ) subscribe(channel) } diff --git a/main/src/main/scala/sbt/internal/SysProp.scala b/main/src/main/scala/sbt/internal/SysProp.scala index 737485965..2f4e95953 100644 --- a/main/src/main/scala/sbt/internal/SysProp.scala +++ b/main/src/main/scala/sbt/internal/SysProp.scala @@ -104,7 +104,9 @@ object SysProp { def dumbTerm: Boolean = sys.env.get("TERM").contains("dumb") def supershell: Boolean = booleanOpt("sbt.supershell").getOrElse(!dumbTerm && color) - def supershellSleep: Long = long("sbt.supershell.sleep", 100L) + def supershellMaxTasks: Int = int("sbt.supershell.maxitems", 8) + def supershellSleep: Long = long("sbt.supershell.sleep", 500.millis.toMillis) + def supershellThreshold: FiniteDuration = long("sbt.supershell.threshold", 100L).millis def supershellBlankZone: Int = int("sbt.supershell.blankzone", 1) def defaultUseCoursier: Boolean = { diff --git a/main/src/main/scala/sbt/internal/TaskProgress.scala b/main/src/main/scala/sbt/internal/TaskProgress.scala index f292db125..0c3cfbc72 100644 --- a/main/src/main/scala/sbt/internal/TaskProgress.scala +++ b/main/src/main/scala/sbt/internal/TaskProgress.scala @@ -20,13 +20,11 @@ import java.util.concurrent.{ ConcurrentHashMap, Executors, TimeoutException } /** * implements task progress display on the shell. */ -private[sbt] class TaskProgress +private[sbt] class TaskProgress(sleepDuration: FiniteDuration, threshold: FiniteDuration) extends AbstractTaskExecuteProgress with ExecuteProgress[Task] with AutoCloseable { private[this] val lastTaskCount = new AtomicInteger(0) - private[this] val sleepDuration = SysProp.supershellSleep.millis - private[this] val threshold = 10.millis private[this] val reportLoop = new AtomicReference[AutoCloseable] private[this] val active = new ConcurrentHashMap[Task[_], AutoCloseable] private[this] val nextReport = new AtomicReference(Deadline.now) diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index ca75b369a..1c7d93484 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -49,6 +49,7 @@ import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter } import BasicJsonProtocol._ import Serialization.{ attach, promptChannel } +import sbt.internal.util.ProgressState final class NetworkChannel( val name: String, @@ -58,7 +59,8 @@ final class NetworkChannel( instance: ServerInstance, handlers: Seq[ServerHandler], val log: Logger, - mkUIThreadImpl: (State, CommandChannel) => UITask + mkUIThreadImpl: (State, CommandChannel) => UITask, + state: Option[State], ) extends CommandChannel { self => def this( name: String, @@ -77,7 +79,8 @@ final class NetworkChannel( instance, handlers, log, - new UITask.AskUserTask(_, _) + new UITask.AskUserTask(_, _), + None ) private val running = new AtomicBoolean(true) @@ -787,6 +790,10 @@ final class NetworkChannel( ) } private[this] val blockedThreads = ConcurrentHashMap.newKeySet[Thread] + override private[sbt] val progressState: ProgressState = new ProgressState( + 1, + state.flatMap(_.get(Keys.superShellMaxTasks.key)).getOrElse(SysProp.supershellMaxTasks) + ) override def getWidth: Int = getProperty(_.width, 0).getOrElse(0) override def getHeight: Int = getProperty(_.height, 0).getOrElse(0) override def isAnsiSupported: Boolean = getProperty(_.isAnsiSupported, false).getOrElse(false)