Add fast path for parsing commands

It can easily take 2ms or more to parse a command depending on state's
combined parser. There are some commands that sbt requires to work that
we can handle in microseconds instead of milliseconds by special casing
them.

After this change, I saw the performance of
https://github.com/eatkins/scala-build-watch-performance improve by
a consistent 4-5ms in the 3 source file example which was a drop from
120ms to 115ms. While not necessarily earth shattering, this difference
could theoretically be much worse in other projects that have a lot of
plugins and custom tasks/commands. I think it's worth the modest
maintenance cost.
This commit is contained in:
Ethan Atkins 2020-07-27 10:59:57 -07:00
parent 48f086059f
commit caccba7112
3 changed files with 63 additions and 2 deletions

View File

@ -23,6 +23,7 @@ import sbt.util.Logger
import scala.annotation.tailrec
import scala.util.control.NonFatal
import sbt.internal.FastTrackCommands
object MainLoop {
@ -201,7 +202,14 @@ object MainLoop {
StandardMain.exchange.setState(progressState)
StandardMain.exchange.setExec(Some(exec))
StandardMain.exchange.unprompt(ConsoleUnpromptEvent(exec.source))
val newState = Command.process(exec.commandLine, progressState)
/*
* FastTrackCommands.evaluate can be significantly faster than Command.process because
* it avoids an expensive parsing step for internal commands that are easy to parse.
* Dropping (FastTrackCommands.evaluate ... getOrElse) should be functionally identical
* but slower.
*/
val newState = FastTrackCommands.evaluate(progressState, exec.commandLine) getOrElse
Command.process(exec.commandLine, progressState)
// Flush the terminal output after command evaluation to ensure that all output
// is displayed in the thin client before we report the command status.
val terminal = channelName.flatMap(exchange.channelForName(_).map(_.terminal))

View File

@ -1297,7 +1297,7 @@ private[sbt] object ContinuousCommands {
case _ => state
}
}
private[this] val failWatchCommand = watchCommand(failWatch) { (channel, state) =>
private[sbt] val failWatchCommand = watchCommand(failWatch) { (channel, state) =>
state.fail
}
/*

View File

@ -0,0 +1,53 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
import BasicCommandStrings._
import BasicCommands._
import BuiltinCommands.{ setTerminalCommand, shell, waitCmd }
import ContinuousCommands._
import sbt.internal.util.complete.Parser
/** This is used to speed up command parsing. */
private[sbt] object FastTrackCommands {
private def fromCommand(
cmd: String,
command: Command,
arguments: Boolean = true,
): (State, String) => Option[State] =
(s, c) =>
Parser.parse(if (arguments) c else "", command.parser(s)) match {
case Right(newState) => Some(newState())
case l => None
}
private val commands = Map[String, (State, String) => Option[State]](
FailureWall -> { case (s, c) => if (c == FailureWall) Some(s) else None },
StashOnFailure -> fromCommand(StashOnFailure, stashOnFailure, arguments = false),
PopOnFailure -> fromCommand(PopOnFailure, popOnFailure, arguments = false),
Shell -> fromCommand(Shell, shell),
SetTerminal -> fromCommand(SetTerminal, setTerminalCommand),
failWatch -> fromCommand(failWatch, failWatchCommand),
preWatch -> fromCommand(preWatch, preWatchCommand),
postWatch -> fromCommand(postWatch, postWatchCommand),
runWatch -> fromCommand(runWatch, runWatchCommand),
stopWatch -> fromCommand(stopWatch, stopWatchCommand),
waitWatch -> fromCommand(waitWatch, waitCmd),
)
private[sbt] def evaluate(state: State, cmd: String): Option[State] = {
cmd.trim.split(" ") match {
case Array(h, _*) =>
commands.get(h) match {
case Some(command) => command(state, cmd)
case _ => None
}
case _ => None
}
}
}