diff --git a/build.sbt b/build.sbt index cac7e63..dd4c86a 100644 --- a/build.sbt +++ b/build.sbt @@ -159,6 +159,7 @@ lazy val integrations = project .in(file("integrations")) .dependsOn(Circe) .settings( + libraryDependencies += "com.github.alexarchambault" %% "case-app" % "1.2.0", scalaHome := BuildDefaults.setUpScalaHome.value, parallelExecution in Test := false, scalacOptions in Compile := (Def.taskDyn { diff --git a/integrations/src/main/scala/profiling/integrations/caseapp/CliOptions.scala b/integrations/src/main/scala/profiling/integrations/caseapp/CliOptions.scala new file mode 100644 index 0000000..d1fac90 --- /dev/null +++ b/integrations/src/main/scala/profiling/integrations/caseapp/CliOptions.scala @@ -0,0 +1,22 @@ +package profiling.integrations + +import java.nio.file.Path + +import caseapp.{ExtraName, HelpMessage, Recurse, ValueDescription} + +case class CliOptions( + @ExtraName("c") + @HelpMessage("File path to the bloop config directory.") + @ValueDescription(".bloop") + configDir: Option[Path] = None, + @ExtraName("v") + @HelpMessage("If set, print the about section at the beginning of the execution.") + version: Boolean = false, + @HelpMessage("If set, print out debugging information to stderr.") + verbose: Boolean = false, + @Recurse common: CommonOptions = CommonOptions.default, +) + +object CliOptions { + val default = CliOptions() +} diff --git a/integrations/src/main/scala/profiling/integrations/caseapp/Commands.scala b/integrations/src/main/scala/profiling/integrations/caseapp/Commands.scala new file mode 100644 index 0000000..f81852e --- /dev/null +++ b/integrations/src/main/scala/profiling/integrations/caseapp/Commands.scala @@ -0,0 +1,176 @@ +package profiling.integrations + +import java.nio.file.Path + +import caseapp.core.ArgParser +import caseapp.{ArgsName, CommandName, ExtraName, HelpMessage, Hidden, Recurse} +import caseapp.core.CommandMessages + +object Commands { + + /* sealed abstract class Mode(val name: String) + + /** The kind of items that should be returned for autocompletion */ + object Mode { + case object Commands extends Mode("commands") + case object Projects extends Mode("projects") + case object ProjectBoundCommands extends Mode("project-commands") + case object Flags extends Mode("flags") + case object Reporters extends Mode("reporters") + case object Protocols extends Mode("protocols") + case object TestsFQCN extends Mode("testsfqcn") + case object MainsFQCN extends Mode("mainsfqcn") + + implicit val completionModeRead: ArgParser[Mode] = ??? + } + + sealed abstract class BspProtocol(val name: String) + + object BspProtocol { + case object Local extends BspProtocol("local") + case object Tcp extends BspProtocol("tcp") + + implicit val bspProtocolRead: ArgParser[BspProtocol] = ??? + } + + sealed abstract class ReporterKind(val name: String) + case object ScalacReporter extends ReporterKind("scalac") + case object BloopReporter extends ReporterKind("bloop")*/ + + sealed trait RawCommand { + def cliOptions: CliOptions + } + + sealed trait CompilingCommand extends RawCommand { + //def project: String + //def reporter: ReporterKind + } + +/* sealed trait Tree[A] + case class Leaf[A](value: A) extends Tree[A] + case class Branch[A]( + left: Tree[A], + right: Tree[A] + ) extends Tree[A]*/ + + case class Help( + @Recurse cliOptions: CliOptions = CliOptions.default + ) extends RawCommand + + case class Autocomplete( + @Recurse cliOptions: CliOptions = CliOptions.default, + //mode: Mode, + //format: Format, + /* command: Option[String], + project: Option[String]*/ + ) extends RawCommand + + case class About( + @Recurse cliOptions: CliOptions = CliOptions.default + ) extends RawCommand + + case class Projects( + /* @ExtraName("dot") + @HelpMessage("Print out a dot graph you can pipe into `dot`. By default, false.") + dotGraph: Boolean = false,*/ + @Recurse cliOptions: CliOptions = CliOptions.default + ) extends RawCommand + + case class Configure( + /* @ExtraName("parallelism") + @HelpMessage("Set the number of threads used for parallel compilation and test execution.") + threads: Int = 4,*/ + @Recurse cliOptions: CliOptions = CliOptions.default + ) extends RawCommand + + case class Clean( + /* @ExtraName("p") + @HelpMessage("The projects to clean.") + project: List[String] = Nil, + @HelpMessage("Do not run clean for dependencies. By default, false.") + isolated: Boolean = false,*/ + @Recurse cliOptions: CliOptions = CliOptions.default, + ) extends RawCommand + + @CommandName("bsp") + case class Bsp( + /*/* @ExtraName("p") + @HelpMessage("The connection protocol for the bsp server. By default, local.") + protocol: BspProtocol = BspProtocol.Local,*/ + @ExtraName("h") + @HelpMessage("The server host for the bsp server (TCP only).") + host: String = "127.0.0.1", + @HelpMessage("The port for the bsp server (TCP only).") + port: Int = 5101, + @ExtraName("s") + @HelpMessage("A path to a socket file to communicate through Unix sockets (local only).") + socket: Option[Path] = None, + @ExtraName("pn") + @HelpMessage( + "A path to a new existing socket file to communicate through Unix sockets (local only)." + ) + pipeName: Option[String] = None,*/ + @Recurse cliOptions: CliOptions = CliOptions.default + ) extends RawCommand + + case class Compile( + /* @ExtraName("p") + @HelpMessage("The project to compile (will be inferred from remaining cli args).") + project: String = "", + @HelpMessage("Compile the project incrementally. By default, true.") + incremental: Boolean = true, +/* @HelpMessage("Pick reporter to show compilation messages. By default, bloop's used.") + reporter: ReporterKind = BloopReporter,*/ + @ExtraName("w") + @HelpMessage("Run the command when projects' source files change. By default, false.") + watch: Boolean = false,*/ + @Recurse cliOptions: CliOptions = CliOptions.default, + ) extends CompilingCommand + + case class Test( + /* @ExtraName("p") + @HelpMessage("The project to test (will be inferred from remaining cli args).") + project: String = "", + @HelpMessage("Do not run tests for dependencies. By default, false.") + isolated: Boolean = false, + @ExtraName("o") + @HelpMessage("The list of test suite filters to test for only.") + only: List[String] = Nil, + @HelpMessage("The arguments to pass in to the test framework.") + args: List[String] = Nil, +/* @HelpMessage("Pick reporter to show compilation messages. By default, bloop's used.") + reporter: ReporterKind = BloopReporter,*/ + @ExtraName("w") + @HelpMessage("Run the command when projects' source files change. By default, false.") + watch: Boolean = false,*/ + @Recurse cliOptions: CliOptions = CliOptions.default + ) extends CompilingCommand + + case class Console( + /* @ExtraName("p") + @HelpMessage("The project to run the console at (will be inferred from remaining cli args).") + project: String = "", +/* @HelpMessage("Pick reporter to show compilation messages. By default, bloop's used.") + reporter: ReporterKind = BloopReporter,*/ + @HelpMessage("Start up the console compiling only the target project's dependencies.") + excludeRoot: Boolean = false,*/ + @Recurse cliOptions: CliOptions = CliOptions.default + ) extends CompilingCommand + + case class Run( + /* @ExtraName("p") + @HelpMessage("The project to run (will be inferred from remaining cli args).") + project: String = "", + @ExtraName("m") + @HelpMessage("The main class to run. Leave unset to let bloop select automatically.") + main: Option[String] = None, +/* @HelpMessage("Pick reporter to show compilation messages. By default, bloop's used.") + reporter: ReporterKind = BloopReporter,*/ + @HelpMessage("The arguments to pass in to the main class.") + args: List[String] = Nil, + @ExtraName("w") + @HelpMessage("If set, run the command whenever projects' source files change.") + watch: Boolean = false,*/ + @Recurse cliOptions: CliOptions = CliOptions.default + ) extends CompilingCommand +} diff --git a/integrations/src/main/scala/profiling/integrations/caseapp/CommonOptions.scala b/integrations/src/main/scala/profiling/integrations/caseapp/CommonOptions.scala new file mode 100644 index 0000000..6f4b9fe --- /dev/null +++ b/integrations/src/main/scala/profiling/integrations/caseapp/CommonOptions.scala @@ -0,0 +1,50 @@ +package profiling.integrations + +import java.io.{InputStream, PrintStream} +import java.util.Properties + +import caseapp.Hidden + +/** + * Describes the common options for any command or CLI operation. + * + * They exist for two purposes: testing and nailgun. In both cases we + * need a precise handling of these parameters because they change + * depending on the environment we're running on. + * + * They are hidden because they are optional. + */ +case class CommonOptions( + @Hidden workingDirectory: String = System.getProperty("user.dir"), + @Hidden out: PrintStream = System.out, + @Hidden in: InputStream = System.in, + @Hidden err: PrintStream = System.err, + @Hidden ngout: PrintStream = System.out, + @Hidden ngerr: PrintStream = System.err +) + +object CommonOptions { + final val default = CommonOptions() + + // Our own version of properties in which we override `toString` + final class PrettyProperties extends Properties { + override def toString: String = synchronized { + super.keySet().toArray.map(_.toString).mkString(", ") + } + } + + object PrettyProperties { + def from(p: Properties): PrettyProperties = { + val pp = new PrettyProperties() + pp.putAll(p) + pp + } + } + + final lazy val currentEnv: PrettyProperties = { + import scala.collection.JavaConverters._ + System.getenv().asScala.foldLeft(new PrettyProperties()) { + case (props, (key, value)) => props.setProperty(key, value); props + } + } +} diff --git a/integrations/src/main/scala/profiling/integrations/caseapp/Parsers.scala b/integrations/src/main/scala/profiling/integrations/caseapp/Parsers.scala new file mode 100644 index 0000000..9669423 --- /dev/null +++ b/integrations/src/main/scala/profiling/integrations/caseapp/Parsers.scala @@ -0,0 +1,95 @@ +package profiling.integrations + +import java.io.{InputStream, PrintStream} +import java.nio.file.{Path, Paths} + +import CommonOptions.PrettyProperties +import caseapp.{CaseApp, CommandParser} +import caseapp.util.{Implicit, AnnotationList} +import caseapp.core.{ArgParser, DefaultBaseCommand, HListParser, Parser, Default} +import shapeless.{Annotations, Coproduct, LabelledGeneric, Strict, Generic, HList} +import caseapp.{Name, ValueDescription, HelpMessage, Hidden, Recurse} + +import scala.util.Try + +trait CachedImplicits { + implicit val inputStreamRead: ArgParser[InputStream] = + ArgParser.instance[InputStream]("stdin")(_ => Right(System.in)) + implicit val printStreamRead: ArgParser[PrintStream] = + ArgParser.instance[PrintStream]("stdout")(_ => Right(System.out)) + + implicit val pathParser: ArgParser[Path] = ArgParser.instance("A filepath parser") { + case supposedPath: String => + val toPath = Try(Paths.get(supposedPath)).toEither + toPath.left.map(t => s"The provided path ${supposedPath} is not valid: '${t.getMessage()}'.") + } + + implicit val propertiesParser: ArgParser[PrettyProperties] = { + ArgParser.instance("A properties parser") { + case whatever => Left("You cannot pass in properties through the command line.") + } + } + + import shapeless.{HNil, CNil, :+:, ::} + implicit val implicitHNil: Implicit[HNil] = Implicit.hnil + implicit val implicitNone: Implicit[None.type] = Implicit.instance(None) + implicit val implicitNoneCnil: Implicit[None.type :+: CNil] = + Implicit.instance(Coproduct(None)) + + implicit val implicitOptionDefaultString: Implicit[Option[Default[String]]] = + Implicit.instance(Some(caseapp.core.Defaults.string)) + + implicit val implicitOptionDefaultInt: Implicit[Option[Default[Int]]] = + Implicit.instance(Some(caseapp.core.Defaults.int)) + + implicit val implicitOptionDefaultBoolean: Implicit[Option[Default[Boolean]]] = + Implicit.instance(Some(caseapp.core.Defaults.boolean)) + + implicit val implicitDefaultBoolean: Implicit[Default[Boolean]] = + Implicit.instance(caseapp.core.Defaults.boolean) + + implicit val implicitOptionDefaultOptionPath: Implicit[Option[Default[Option[Path]]]] = + Implicit.instance(None) + + implicit val implicitOptionDefaultPrintStream: Implicit[Option[Default[PrintStream]]] = + Implicit.instance(Some(Default.instance[PrintStream](System.out))) + + implicit val implicitOptionDefaultInputStream: Implicit[Option[Default[InputStream]]] = + Implicit.instance(Some(Default.instance[InputStream](System.in))) +} + +object Parsers extends CachedImplicits { + + import shapeless.{the, HNil, ::} + + implicit val labelledGenericCommonOptions: LabelledGeneric.Aux[CommonOptions, _] = LabelledGeneric.materializeProduct + implicit val commonOptionsParser: Parser.Aux[CommonOptions, _] = Parser.generic + implicit val labelledGenericCliOptions: LabelledGeneric.Aux[CliOptions, _] = LabelledGeneric.materializeProduct + implicit val cliOptionsParser: Parser.Aux[CliOptions, _] = Parser.generic + + implicit val strictAutocompleteParser: Parser.Aux[Commands.Autocomplete, _] = Parser.generic + implicit val strictAboutParser: Parser.Aux[Commands.About, _] = Parser.generic + implicit val strictBspParser: Parser.Aux[Commands.Bsp, _] = Parser.generic + implicit val strictCleanParser: Parser.Aux[Commands.Clean, _] = Parser.generic + implicit val strictCompileParser: Parser.Aux[Commands.Compile, _] = Parser.generic + implicit val strictConfigureParser: Parser.Aux[Commands.Configure, _] = Parser.generic + implicit val strictConsoleParser: Parser.Aux[Commands.Console, _] = Parser.generic + implicit val strictHelpParser: Parser.Aux[Commands.Help, _] = Parser.generic + implicit val strictProjectsParser: Parser.Aux[Commands.Projects, _] = Parser.generic + implicit val strictRunParser: Parser.Aux[Commands.Run, _] = Parser.generic + implicit val strictTestParser: Parser.Aux[Commands.Test, _] = Parser.generic + + val BaseMessages: caseapp.core.Messages[DefaultBaseCommand] = + caseapp.core.Messages[DefaultBaseCommand] + val CommandsMessages: caseapp.core.CommandsMessages[Commands.RawCommand] = + implicitly[caseapp.core.CommandsMessages[Commands.RawCommand]] + val CommandsParser: CommandParser[Commands.RawCommand] = + implicitly[caseapp.core.CommandParser[Commands.RawCommand]] +} + +object Main extends App { + import Parsers._ +/* assert(CommandsParser != null) + assert(CommandsMessages != null)*/ + println("Hello World!") +} diff --git a/integrations/src/main/scala/profiling/integrations/circe/WebsiteExample.scala b/integrations/src/main/scala/profiling/integrations/circe/WebsiteExample.scala index e2b6f30..bc69d0d 100644 --- a/integrations/src/main/scala/profiling/integrations/circe/WebsiteExample.scala +++ b/integrations/src/main/scala/profiling/integrations/circe/WebsiteExample.scala @@ -1,4 +1,4 @@ -import io.circe._ +/*import io.circe._ import io.circe.generic.auto._ import io.circe.parser._ import io.circe.syntax._ @@ -16,4 +16,4 @@ object WebsiteExample extends App { import io.circe.generic.semiauto._ implicit val fooDecoder: Decoder[Foo] = deriveDecoder[Foo] implicit val fooEncoder: Encoder[Foo] = deriveEncoder[Foo]*/ -} +}*/ diff --git a/plugin/src/main/scala/ch/epfl/scala/PluginConfig.scala b/plugin/src/main/scala/ch/epfl/scala/PluginConfig.scala index d9b56a7..3ad75a5 100644 --- a/plugin/src/main/scala/ch/epfl/scala/PluginConfig.scala +++ b/plugin/src/main/scala/ch/epfl/scala/PluginConfig.scala @@ -8,5 +8,6 @@ case class PluginConfig( sourceRoot: Option[AbsolutePath], printSearchIds: Set[Int], generateMacroFlamegraph: Boolean, + printFailedMacroImplicits: Boolean, concreteTypeParamsInImplicits: Boolean ) diff --git a/plugin/src/main/scala/ch/epfl/scala/ProfilingPlugin.scala b/plugin/src/main/scala/ch/epfl/scala/ProfilingPlugin.scala index bf69257..a3f3a6d 100644 --- a/plugin/src/main/scala/ch/epfl/scala/ProfilingPlugin.scala +++ b/plugin/src/main/scala/ch/epfl/scala/ProfilingPlugin.scala @@ -34,6 +34,7 @@ class ProfilingPlugin(val global: Global) extends Plugin { private final lazy val SourceRoot = "sourceroot" private final lazy val PrintSearchResult = "print-search-result" private final lazy val GenerateMacroFlamegraph = "generate-macro-flamegraph" + private final lazy val PrintFailedMacroImplicits = "print-failed-implicit-macro-candidates" private final lazy val NoProfileDb = "no-profiledb" private final lazy val ShowConcreteImplicitTparams = "show-concrete-implicit-tparams" private final lazy val PrintSearchRegex = s"$PrintSearchResult:(.*)".r @@ -59,6 +60,7 @@ class ProfilingPlugin(val global: Global) extends Plugin { findOption(SourceRoot, SourceRootRegex).map(AbsolutePath.apply), findSearchIds(findOption(PrintSearchResult, PrintSearchRegex)), super.options.contains(GenerateMacroFlamegraph), + super.options.contains(PrintFailedMacroImplicits), super.options.contains(ShowConcreteImplicitTparams) ) diff --git a/plugin/src/main/scala/ch/epfl/scala/profilers/ProfilingImpl.scala b/plugin/src/main/scala/ch/epfl/scala/profilers/ProfilingImpl.scala index 20910c2..41e78a9 100644 --- a/plugin/src/main/scala/ch/epfl/scala/profilers/ProfilingImpl.scala +++ b/plugin/src/main/scala/ch/epfl/scala/profilers/ProfilingImpl.scala @@ -330,8 +330,8 @@ final class ProfilingImpl[G <: Global]( sys.error(s"Missing $name for $searchId ($targetType).") val forcedExpansions = - ProfilingMacroPlugin.searchIdsToMacroStates.getOrElse(searchId, Nil).size - val expandedStr = s"(expanded macros $forcedExpansions)" + ProfilingMacroPlugin.searchIdsToMacroStates.getOrElse(searchId, Nil) + val expandedStr = s"(expanded macros ${forcedExpansions.size})" // Detect macro name if the type we get comes from a macro to add it to the stack val suffix = { @@ -361,10 +361,19 @@ final class ProfilingImpl[G <: Global]( else concreteTypeFromSearch(result.subst(result.tree), targetType) } - if (config.printSearchIds.contains(searchId)) { - logger.info(s"""Showing tree of implicit search ${searchId} of type `${typeForStack}`: + if (config.printSearchIds.contains(searchId) || (result.isFailure && config.printFailedMacroImplicits)) { + logger.info( + s"""implicit search ${searchId}: + | -> valid ${result.isSuccess} + | -> type `${typeForStack}` + | -> ${search.undet_s} + | -> ${search.ctx_s} + | -> tree: |${showCode(result.tree)} - |""".stripMargin) + | -> forced expansions: + |${forcedExpansions.mkString(" ", " \n", "\n")} + |""".stripMargin + ) } val thisStackName = s"${typeToString(typeForStack)}$suffix" @@ -438,15 +447,16 @@ final class ProfilingImpl[G <: Global]( private val macroIdsToTimers = perRunCaches.newMap[Int, statistics.Timer]() private val macroChildren = perRunCaches.newMap[Int, List[MacroEntry]]() private val stackedNanos = perRunCaches.newMap[Int, Long]() - private val stackedNames = perRunCaches.newMap[Int, String]() + private val stackedNames = perRunCaches.newMap[Int, List[String]]() def foldMacroStacks(outputPath: Path): Unit = { // This part is memory intensive and hence the use of java collections val stacksJavaList = new java.util.ArrayList[String]() stackedNanos.foreach { case (id, nanos) => - val stackName = + val names = stackedNames.getOrElse(id, sys.error(s"Stack name for macro id ${id} doesn't exist!")) + val stackName = names.mkString(";") stacksJavaList.add(s"$stackName ${nanos / 1000}") } java.util.Collections.sort(stacksJavaList) @@ -520,16 +530,15 @@ final class ProfilingImpl[G <: Global]( case None => sys.error("Fatal error: macro has no state!") } - stackedNames.update(macroId, thisStackName) + stackedNames.update(macroId, thisStackName :: Nil) children.foreach { childSearch => val id = childSearch.id val childrenStackName = stackedNames.getOrElse(id, sys.error("no stack name")) - stackedNames.update(id, s"$thisStackName;$childrenStackName") + stackedNames.update(id, thisStackName :: childrenStackName) } } statistics.stopTimer(macroTimer, head.start) - // TODO: Why are we getting the previous nanos first? val previousNanos = stackedNanos.getOrElse(macroId, 0L) stackedNanos.+=((macroId, macroTimer.nanos + previousNanos)) prevData match { @@ -571,7 +580,6 @@ final class ProfilingImpl[G <: Global]( super.onFailure(expanded) } - override def onSkipped(expanded: Tree) = { val state = SkippedMacro(pt, expanded) mapToCurrentImplicitSearch(state)