From caccba711206c0ef773bd67a63a96d24a7d49a8b Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Mon, 27 Jul 2020 10:59:57 -0700 Subject: [PATCH] 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. --- main/src/main/scala/sbt/MainLoop.scala | 10 +++- .../main/scala/sbt/internal/Continuous.scala | 2 +- .../sbt/internal/FastTrackCommands.scala | 53 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 main/src/main/scala/sbt/internal/FastTrackCommands.scala diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index a2c4efd7d..99256ed79 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -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)) diff --git a/main/src/main/scala/sbt/internal/Continuous.scala b/main/src/main/scala/sbt/internal/Continuous.scala index 557fa3300..cd6682433 100644 --- a/main/src/main/scala/sbt/internal/Continuous.scala +++ b/main/src/main/scala/sbt/internal/Continuous.scala @@ -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 } /* diff --git a/main/src/main/scala/sbt/internal/FastTrackCommands.scala b/main/src/main/scala/sbt/internal/FastTrackCommands.scala new file mode 100644 index 000000000..8c53f81de --- /dev/null +++ b/main/src/main/scala/sbt/internal/FastTrackCommands.scala @@ -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 + } + } +}