Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functionality to build a global flamegraph of implicit searches #50

Merged
merged 6 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ch.epfl.scala.profilers.tools

object ScalaSettingsOps {
def isScala212: Boolean = true
def isScala213: Boolean = false
Comment on lines +3 to +5
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a bit controversial, perhaps there is a more elegant way to do these checks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for the context, scala/scala#9575.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ch.epfl.scala.profilers.tools

object ScalaSettingsOps {
def isScala212: Boolean = false
def isScala213: Boolean = true
}
5 changes: 3 additions & 2 deletions plugin/src/main/scala/ch/epfl/scala/PluginConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package ch.epfl.scala

import ch.epfl.scala.profiledb.utils.AbsolutePath

case class PluginConfig(
final case class PluginConfig(
showProfiles: Boolean,
generateDb: Boolean,
sourceRoot: Option[AbsolutePath],
sourceRoot: AbsolutePath,
printSearchIds: Set[Int],
generateMacroFlamegraph: Boolean,
generateGlobalFlamegraph: Boolean,
printFailedMacroImplicits: Boolean,
concreteTypeParamsInImplicits: Boolean
)
43 changes: 35 additions & 8 deletions plugin/src/main/scala/ch/epfl/scala/ProfilingPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@
package ch.epfl.scala

import java.nio.file.Files

import ch.epfl.scala.profiledb.{ProfileDb, ProfileDbPath}
import ch.epfl.scala.profiledb.utils.AbsolutePath
import ch.epfl.scala.profiledb.utils.{AbsolutePath, RelativePath}
import ch.epfl.scala.profilers.ProfilingImpl
import ch.epfl.scala.profilers.tools.{Logger, SettingsOps}
import ch.epfl.scala.profilers.tools.{Logger, ScalaSettingsOps, SettingsOps}

import scala.reflect.internal.util.{SourceFile, Statistics}
import scala.reflect.io.Path
Expand All @@ -34,6 +33,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 GenerateGlobalFlamegraph = "generate-global-flamegraph"
private final lazy val PrintFailedMacroImplicits = "print-failed-implicit-macro-candidates"
private final lazy val GenerateProfileDb = "generate-profiledb"
private final lazy val ShowConcreteImplicitTparams = "show-concrete-implicit-tparams"
Expand All @@ -57,18 +57,25 @@ class ProfilingPlugin(val global: Global) extends Plugin {
private final lazy val config = PluginConfig(
showProfiles = super.options.contains(ShowProfiles),
generateDb = super.options.contains(GenerateProfileDb),
sourceRoot = findOption(SourceRoot, SourceRootRegex).map(AbsolutePath.apply),
sourceRoot =
findOption(SourceRoot, SourceRootRegex)
.map(AbsolutePath.apply)
.getOrElse(AbsolutePath.workingDirectory),
printSearchIds = findSearchIds(findOption(PrintSearchResult, PrintSearchRegex)),
generateMacroFlamegraph = super.options.contains(GenerateMacroFlamegraph),
generateGlobalFlamegraph = super.options.contains(GenerateGlobalFlamegraph),
printFailedMacroImplicits = super.options.contains(PrintFailedMacroImplicits),
concreteTypeParamsInImplicits = super.options.contains(ShowConcreteImplicitTparams)
)

private lazy val logger = new Logger(global)

private def pad20(option: String): String = option + (" " * (20 - option.length))

override def init(ops: List[String], e: (String) => Unit): Boolean = true

override val optionsHelp: Option[String] = Some(s"""
|-P:$name:${pad20(GenerateGlobalFlamegraph)}: Creates a global flamegraph of implicit searches for all compilation units. Use the `-P:$name:$SourceRoot` option to manage the root directory, otherwise, a working directory (defined by the `user.dir` property) will be picked.
|-P:$name:${pad20(SourceRoot)}:_ Sets the source root for this project.
|-P:$name:${pad20(ShowProfiles)} Logs profile information for every call-site.
|-P:$name:${pad20(ShowConcreteImplicitTparams)} Shows types in flamegraphs of implicits with concrete type params.
Expand Down Expand Up @@ -96,10 +103,31 @@ class ProfilingPlugin(val global: Global) extends Plugin {
}

private def reportStatistics(graphsPath: AbsolutePath): Unit = {
val macroProfiler = implementation.macroProfiler
val persistedGraphData = implementation.generateGraphData(graphsPath)
val globalDir =
if (config.generateGlobalFlamegraph) {
val scalaDir =
if (ScalaSettingsOps.isScala212)
"scala-2.12"
else if (ScalaSettingsOps.isScala213)
"scala-2.13"
else
sys.error(s"Currently, only Scala 2.12 and 2.13 are supported, " +
s"but [${global.settings.source.value}] has been spotted")

val globalDir =
ProfileDbPath.toGraphsProfilePath(
config.sourceRoot.resolve(RelativePath(s"target/$scalaDir/classes"))
Copy link
Contributor

@lolgab lolgab Nov 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The target directory is customizable in sbt. This doesn't work if you customize it. Maybe the plugin should receive the ScalaDir directory as parameter which opens the door to use the plugin also in other build tools like mill where the directory structure is different.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I thought about this one. If we agree on the approach, I will iterate on this. From my understanding, scalac-profiling was designed for SBT mostly. There are also a few other things related to SBT's output.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my projects at work I have:

// avoid invalidating compilation cache when enabling/disabling coverage
target := { if (ScoverageKeys.coverageEnabled.value) target.value / "coverage" else target.value }

which will break here. But we don't need to fix this problem now. It can be iterated over later :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lolgab but this would also change targets for particular modules, and currently generated flamegraphs will be put into the coverage space, won't it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. but your hardcoding of target will not work.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So perhaps we need to generalize the task and make target configurable overall, wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but your hardcoding of target will not work.

btw, have you tried specifying the source root directory?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can ask for crossTarget and then look inside for the classes.
For example it can be something like: s"-P:scalac-profiling:crossTarget=${crossTarget.value}".
crossTarget points by default to target/scala-2.13 but it can be configured and matches what sbt uses.
If you change target sbt automatically changes crossTarget.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But yeah, it doesn't need to be done in this PR. Just something to keep in mind for later.

)

Some(globalDir)
} else None

val persistedGraphData = implementation.generateGraphData(graphsPath, globalDir)
persistedGraphData.foreach(p => logger.info(s"Writing graph to ${p.underlying}"))

if (config.showProfiles) {
val macroProfiler = implementation.macroProfiler

logger.info("Macro data per call-site", macroProfiler.perCallSite)
logger.info("Macro data per file", macroProfiler.perFile)
logger.info("Macro data in total", macroProfiler.inTotal)
Expand Down Expand Up @@ -181,10 +209,9 @@ class ProfilingPlugin(val global: Global) extends Plugin {
if (outputPath.isEmpty) "." else outputPath
}

private final def sourceRoot = config.sourceRoot.getOrElse(AbsolutePath.workingDirectory)
private def dbPathFor(sourceFile: SourceFile): Option[ProfileDbPath] = {
val absoluteSourceFile = AbsolutePath(sourceFile.file.path)
val targetPath = absoluteSourceFile.toRelative(sourceRoot)
val targetPath = absoluteSourceFile.toRelative(config.sourceRoot)
if (targetPath.syntax.endsWith(".scala")) {
val outputDir = getOutputDirFor(sourceFile.file)
val absoluteOutput = AbsolutePath(outputDir.jfile)
Expand Down
119 changes: 81 additions & 38 deletions plugin/src/main/scala/ch/epfl/scala/profilers/ProfilingImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -149,20 +149,45 @@ final class ProfilingImpl[G <: Global](
}
}

def generateGraphData(outputDir: AbsolutePath): List[AbsolutePath] = {
def generateGraphData(outputDir: AbsolutePath, globalDirMaybe: Option[AbsolutePath]): List[AbsolutePath] = {
Files.createDirectories(outputDir.underlying)

val randomId = java.lang.Long.toString(System.currentTimeMillis())
val implicitGraphName = s"implicit-searches-$randomId"
val macroGraphName = s"macros-$randomId"
/* val dotFile = outputDir.resolve(s"$graphName.dot")

/*val dotFile = outputDir.resolve(s"$graphName.dot")
ProfilingAnalyzerPlugin.dottify(graphName, dotFile.underlying)*/
val implicitFlamegraphFile = outputDir.resolve(s"$implicitGraphName.flamegraph")
ProfilingAnalyzerPlugin.foldImplicitStacks(implicitFlamegraphFile.underlying)
if (config.generateMacroFlamegraph) {
val macroFlamegraphFile = outputDir.resolve(s"$macroGraphName.flamegraph")
ProfilingMacroPlugin.foldMacroStacks(macroFlamegraphFile.underlying)
List(implicitFlamegraphFile, macroFlamegraphFile)
} else List(implicitFlamegraphFile)

val implicitFlamegraphFiles = {
val mkImplicitGraphName: String => String =
postfix => s"implicit-searches-$postfix.flamegraph"
val compileUnitFlamegraphFile = outputDir.resolve(mkImplicitGraphName(randomId))

globalDirMaybe match {
case Some(globalDir) =>
Files.createDirectories(globalDir.underlying)

val globalFile =
globalDir
.resolve(mkImplicitGraphName("global"))

List(compileUnitFlamegraphFile, globalFile)

case None =>
List(compileUnitFlamegraphFile)
}
}

val macroFlamegraphFiles =
if (config.generateMacroFlamegraph) {
val macroGraphName = s"macros-$randomId"
val file = outputDir.resolve(s"$macroGraphName.flamegraph")
List(file)
} else Nil

ProfilingAnalyzerPlugin.foldImplicitStacks(implicitFlamegraphFiles)
ProfilingMacroPlugin.foldMacroStacks(macroFlamegraphFiles)

implicitFlamegraphFiles ::: macroFlamegraphFiles
}

private val registeredQuantities = QuantitiesHijacker.getRegisteredQuantities(global)
Expand Down Expand Up @@ -190,20 +215,29 @@ final class ProfilingImpl[G <: Global](
private val implicitsDependants = new mutable.AnyRefMap[Type, mutable.HashSet[Type]]()
private val searchIdChildren = perRunCaches.newMap[Int, List[analyzer.ImplicitSearch]]()

def foldImplicitStacks(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, tpe)) =>
val names =
stackedNames.getOrElse(id, sys.error(s"Stack name for search id ${id} doesn't exist!"))
val stackName = names.mkString(";")
//val count = implicitSearchesByType.getOrElse(tpe, sys.error(s"No counter for ${tpe}"))
stacksJavaList.add(s"$stackName ${nanos / 1000}")
}
java.util.Collections.sort(stacksJavaList)
Files.write(outputPath, stacksJavaList, StandardOpenOption.WRITE, StandardOpenOption.CREATE)
}
def foldImplicitStacks(outputPaths: Seq[AbsolutePath]): Unit =
if (outputPaths.nonEmpty) {
// This part is memory intensive and hence the use of java collections
val stacksJavaList = new java.util.ArrayList[String]()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider using a scala.collection.mutable.ArrayBuffer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this as is (as Jorge implemented it initially). But I also felt that we can change this collection over time.

stackedNanos.foreach {
case (id, (nanos, _)) =>
val names =
stackedNames.getOrElse(id, sys.error(s"Stack name for search id ${id} doesn't exist!"))
val stackName = names.mkString(";")
//val count = implicitSearchesByType.getOrElse(tpe, sys.error(s"No counter for ${tpe}"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this commented out line needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preserved it as a historical artefact 🤷🏻

stacksJavaList.add(s"$stackName ${nanos / 1000}")
}
java.util.Collections.sort(stacksJavaList)

outputPaths.foreach(path =>
Files.write(
path.underlying,
stacksJavaList,
StandardOpenOption.APPEND,
StandardOpenOption.CREATE
)
)
} else ()

def dottify(graphName: String, outputPath: Path): Unit = {
def clean(`type`: Type) = typeToString(`type`).replace("\"", "\'")
Expand Down Expand Up @@ -454,19 +488,28 @@ final class ProfilingImpl[G <: Global](
private val stackedNanos = perRunCaches.newMap[Int, Long]()
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 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)
Files.write(outputPath, stacksJavaList, StandardOpenOption.WRITE, StandardOpenOption.CREATE)
}
def foldMacroStacks(outputPaths: Seq[AbsolutePath]): Unit =
if (outputPaths.nonEmpty) {
// 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 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)

outputPaths.foreach(path =>
Files.write(
path.underlying,
stacksJavaList,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE
)
)
} else ()

import scala.tools.nsc.Mode
override def pluginsMacroExpand(t: Typer, expandee: Tree, md: Mode, pt: Type): Option[Tree] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ object ProfileDbPath {
def toProfileDbPath(relativeSourceFile: RelativePath): RelativePath =
Prefix.resolveRelative(addDbExtension(relativeSourceFile))

def toGraphsProfilePath(path: AbsolutePath): AbsolutePath =
path.resolve(GraphsProfileDbRelativePath)

private[profiledb] def addDbExtension(path: RelativePath): RelativePath = {
val realPath = path.underlying
val extendedName = realPath.getFileName.toString + ProfileDbExtension
Expand Down
Loading