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.
This commit is contained in:
Ethan Atkins 2020-08-09 17:39:15 -07:00
parent 102e3d1969
commit d58aab5d84
10 changed files with 67 additions and 21 deletions

View File

@ -26,14 +26,16 @@ private[sbt] final class ProgressState(
val padding: AtomicInteger, val padding: AtomicInteger,
val blankZone: Int, val blankZone: Int,
val currentLineBytes: AtomicReference[ArrayBuffer[Byte]], val currentLineBytes: AtomicReference[ArrayBuffer[Byte]],
val maxItems: Int,
) { ) {
def this(blankZone: Int) = def this(blankZone: Int, maxItems: Int) = this(
this(
new AtomicReference(Nil), new AtomicReference(Nil),
new AtomicInteger(0), new AtomicInteger(0),
blankZone, blankZone,
new AtomicReference(new ArrayBuffer[Byte]), new AtomicReference(new ArrayBuffer[Byte]),
maxItems,
) )
def this(blankZone: Int) = this(blankZone, 8)
def currentLine: Option[String] = def currentLine: Option[String] =
new String(currentLineBytes.get.toArray, "UTF-8").linesIterator.toSeq.lastOption new String(currentLineBytes.get.toArray, "UTF-8").linesIterator.toSeq.lastOption
.map(EscHelpers.stripColorsAndMoves) .map(EscHelpers.stripColorsAndMoves)
@ -162,14 +164,18 @@ private[sbt] object ProgressState {
terminal.withPrintStream { ps => terminal.withPrintStream { ps =>
val commandFromThisTerminal = pe.channelName.fold(true)(_ == terminal.name) val commandFromThisTerminal = pe.channelName.fold(true)(_ == terminal.name)
val info = if (commandFromThisTerminal) { val info = if (commandFromThisTerminal) {
pe.items.map { item => val base = pe.items.map { item =>
val elapsed = item.elapsedMicros / 1000000L val elapsed = item.elapsedMicros / 1000000L
s" | => ${item.name} ${elapsed}s" 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 { } else {
pe.command.toSeq.flatMap { cmd => pe.command.toSeq.flatMap { cmd =>
val width = terminal.getWidth 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 if (SERVER_IS_RUNNING_LENGTH + cmd.length < width) cmd
else cmd.take(MIN_COMMAND_WIDTH) + "..." else cmd.take(MIN_COMMAND_WIDTH) + "..."
} else cmd } else cmd

View File

@ -141,7 +141,7 @@ trait Terminal extends AutoCloseable {
private[sbt] def withPrintStream[T](f: PrintStream => T): T private[sbt] def withPrintStream[T](f: PrintStream => T): T
private[sbt] def withRawOutput[R](f: => R): R private[sbt] def withRawOutput[R](f: => R): R
private[sbt] def restore(): Unit = {} 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[this] val promptHolder: AtomicReference[Prompt] = new AtomicReference(Prompt.Pending)
private[sbt] final def prompt: Prompt = promptHolder.get private[sbt] final def prompt: Prompt = promptHolder.get
private[sbt] final def setPrompt(newPrompt: Prompt): Unit = private[sbt] final def setPrompt(newPrompt: Prompt): Unit =
@ -315,6 +315,7 @@ object Terminal {
private[this] object ProxyTerminal extends Terminal { private[this] object ProxyTerminal extends Terminal {
private def t: Terminal = activeTerminal.get private def t: Terminal = activeTerminal.get
override private[sbt] def progressState: ProgressState = t.progressState
override def getWidth: Int = t.getWidth override def getWidth: Int = t.getWidth
override def getHeight: Int = t.getHeight override def getHeight: Int = t.getHeight
override def getLineHeightAndWidth(line: String): (Int, Int) = t.getLineHeightAndWidth(line) override def getLineHeightAndWidth(line: String): (Int, Int) = t.getLineHeightAndWidth(line)
@ -755,6 +756,9 @@ object Terminal {
private val capabilityMap = private val capabilityMap =
org.jline.utils.InfoCmp.Capability.values().map(c => c.toString -> c).toMap 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") @deprecated("For compatibility only", "1.4.0")
private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine 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") private[this] val isCI = sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI")
override lazy val isAnsiSupported: Boolean = term.isAnsiSupported && !isCI override lazy val isAnsiSupported: Boolean = term.isAnsiSupported && !isCI
override private[sbt] def progressState: ProgressState = consoleProgressState.get
override def isEchoEnabled: Boolean = system.echo() override def isEchoEnabled: Boolean = system.echo()
override def isSuccessEnabled: Boolean = true override def isSuccessEnabled: Boolean = true
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
@ -912,6 +917,7 @@ object Terminal {
new WriteableInputStream(nullInputStream, "null-writeable-input-stream") new WriteableInputStream(nullInputStream, "null-writeable-input-stream")
private[sbt] val NullTerminal = new Terminal { private[sbt] val NullTerminal = new Terminal {
override def close(): Unit = {} override def close(): Unit = {}
override private[sbt] def progressState: ProgressState = new ProgressState(1)
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = false override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = false
override def getHeight: Int = 0 override def getHeight: Int = 0
override def getLastLine: Option[String] = None override def getLastLine: Option[String] = None

View File

@ -97,7 +97,7 @@ import sjsonnew._
import sjsonnew.support.scalajson.unsafe.Converter import sjsonnew.support.scalajson.unsafe.Converter
import scala.collection.immutable.ListMap import scala.collection.immutable.ListMap
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration._
import scala.util.control.NonFatal import scala.util.control.NonFatal
import scala.xml.NodeSeq import scala.xml.NodeSeq
@ -393,11 +393,15 @@ object Defaults extends BuildCommon {
else appConfiguration.value.provider.scalaProvider.launcher.topLoader.getParent else appConfiguration.value.provider.scalaProvider.launcher.topLoader.getParent
}, },
useSuperShell := { if (insideCI.value) false else Terminal.console.isSupershellEnabled }, useSuperShell := { if (insideCI.value) false else Terminal.console.isSupershellEnabled },
superShellThreshold :== SysProp.supershellThreshold,
superShellMaxTasks :== SysProp.supershellMaxTasks,
superShellSleep :== SysProp.supershellSleep.millis,
progressReports := { progressReports := {
val rs = EvaluateTask.taskTimingProgress.toVector ++ EvaluateTask.taskTraceEvent.toVector val rs = EvaluateTask.taskTimingProgress.toVector ++ EvaluateTask.taskTraceEvent.toVector
rs map { Keys.TaskProgress(_) } rs map { Keys.TaskProgress(_) }
}, },
progressState := Some(new ProgressState(SysProp.supershellBlankZone)), // progressState is deprecated
SettingKey[Option[ProgressState]]("progressState") := None,
Previous.cache := new Previous( Previous.cache := new Previous(
Def.streamsManagerKey.value, Def.streamsManagerKey.value,
Previous.references.value.getReferences Previous.references.value.getReferences

View File

@ -557,9 +557,13 @@ object Keys {
private[sbt] val currentTaskProgress = AttributeKey[TaskProgress]("current-task-progress") private[sbt] val currentTaskProgress = AttributeKey[TaskProgress]("current-task-progress")
private[sbt] val taskProgress = AttributeKey[sbt.internal.TaskProgress]("active-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 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.") val turbo = settingKey[Boolean]("Enables (true) or disables optional performance features.")
// This key can be used to add custom ExecuteProgress instances // 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) 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 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) private[sbt] val postProgressReports = settingKey[Unit]("Internally used to modify logger.").withRank(DTask)
@deprecated("No longer used", "1.3.0") @deprecated("No longer used", "1.3.0")

View File

@ -935,13 +935,25 @@ object BuiltinCommands {
// This is a workaround for the console task in dotty which uses the classloader cache. // 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. // 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)) 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 => private val setupGlobalFileTreeRepository: State => State = { state =>
state.get(sbt.nio.Keys.globalFileTreeRepository).foreach(_.close()) state.get(sbt.nio.Keys.globalFileTreeRepository).foreach(_.close())
state.put(sbt.nio.Keys.globalFileTreeRepository, FileTreeRepository.default) 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) => { private val addCacheStoreFactoryFactory: State => State = (s: State) => {
val size = Project val size = Project
.extract(s) .extract(s)

View File

@ -22,8 +22,10 @@ import sbt.protocol._
import sbt.util.{ Logger, LoggerContext } import sbt.util.{ Logger, LoggerContext }
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.concurrent.duration._
import scala.util.control.NonFatal import scala.util.control.NonFatal
import sbt.internal.FastTrackCommands import sbt.internal.FastTrackCommands
import sbt.internal.SysProp
object MainLoop { object MainLoop {
@ -150,7 +152,11 @@ object MainLoop {
def next(state: State): State = { def next(state: State): State = {
val context = LoggerContext(useLog4J = state.get(Keys.useLog4J.key).getOrElse(false)) 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 { try {
ErrorHandling.wideConvert { ErrorHandling.wideConvert {
state state

View File

@ -201,7 +201,8 @@ private[sbt] final class CommandExchange {
instance, instance,
handlers, handlers,
s.log, s.log,
mkAskUser(name) mkAskUser(name),
Option(lastState.get),
) )
subscribe(channel) subscribe(channel)
} }

View File

@ -104,7 +104,9 @@ object SysProp {
def dumbTerm: Boolean = sys.env.get("TERM").contains("dumb") def dumbTerm: Boolean = sys.env.get("TERM").contains("dumb")
def supershell: Boolean = booleanOpt("sbt.supershell").getOrElse(!dumbTerm && color) 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 supershellBlankZone: Int = int("sbt.supershell.blankzone", 1)
def defaultUseCoursier: Boolean = { def defaultUseCoursier: Boolean = {

View File

@ -20,13 +20,11 @@ import java.util.concurrent.{ ConcurrentHashMap, Executors, TimeoutException }
/** /**
* implements task progress display on the shell. * implements task progress display on the shell.
*/ */
private[sbt] class TaskProgress private[sbt] class TaskProgress(sleepDuration: FiniteDuration, threshold: FiniteDuration)
extends AbstractTaskExecuteProgress extends AbstractTaskExecuteProgress
with ExecuteProgress[Task] with ExecuteProgress[Task]
with AutoCloseable { with AutoCloseable {
private[this] val lastTaskCount = new AtomicInteger(0) 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 reportLoop = new AtomicReference[AutoCloseable]
private[this] val active = new ConcurrentHashMap[Task[_], AutoCloseable] private[this] val active = new ConcurrentHashMap[Task[_], AutoCloseable]
private[this] val nextReport = new AtomicReference(Deadline.now) private[this] val nextReport = new AtomicReference(Deadline.now)

View File

@ -49,6 +49,7 @@ import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter }
import BasicJsonProtocol._ import BasicJsonProtocol._
import Serialization.{ attach, promptChannel } import Serialization.{ attach, promptChannel }
import sbt.internal.util.ProgressState
final class NetworkChannel( final class NetworkChannel(
val name: String, val name: String,
@ -58,7 +59,8 @@ final class NetworkChannel(
instance: ServerInstance, instance: ServerInstance,
handlers: Seq[ServerHandler], handlers: Seq[ServerHandler],
val log: Logger, val log: Logger,
mkUIThreadImpl: (State, CommandChannel) => UITask mkUIThreadImpl: (State, CommandChannel) => UITask,
state: Option[State],
) extends CommandChannel { self => ) extends CommandChannel { self =>
def this( def this(
name: String, name: String,
@ -77,7 +79,8 @@ final class NetworkChannel(
instance, instance,
handlers, handlers,
log, log,
new UITask.AskUserTask(_, _) new UITask.AskUserTask(_, _),
None
) )
private val running = new AtomicBoolean(true) private val running = new AtomicBoolean(true)
@ -787,6 +790,10 @@ final class NetworkChannel(
) )
} }
private[this] val blockedThreads = ConcurrentHashMap.newKeySet[Thread] 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 getWidth: Int = getProperty(_.width, 0).getOrElse(0)
override def getHeight: Int = getProperty(_.height, 0).getOrElse(0) override def getHeight: Int = getProperty(_.height, 0).getOrElse(0)
override def isAnsiSupported: Boolean = getProperty(_.isAnsiSupported, false).getOrElse(false) override def isAnsiSupported: Boolean = getProperty(_.isAnsiSupported, false).getOrElse(false)