-
Notifications
You must be signed in to change notification settings - Fork 18
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
Changes from all commits
516358a
e1c8013
2d6b21c
4081b75
241c7a3
1f4801c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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" | ||
|
@@ -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. | ||
|
@@ -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")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. but your hardcoding of There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
btw, have you tried specifying the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can ask for There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -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]() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you consider using a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this commented out line needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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("\"", "\'") | ||
|
@@ -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] = { | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.