Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
ch.epfl.scala.ProfilingPlugin.scala Maven / Gradle / Ivy
/* *\
** _____ __ ______ __ **
** / ___/_________ _/ /___ _ / ____/__ ____ / /____ _____ **
** \__ \/ ___/ __ `/ / __ `/ / / / _ \/ __ \/ __/ _ \/ ___/ Scala Center **
** ___/ / /__/ /_/ / / /_/ / / /___/ /__/ / / / /_/ /__/ / https://scala.epfl.ch **
** /____/\___/\__,_/_/\__,_/ \____/\___/_/ /_/\__/\___/_/ (c) 2017-2018, LAMP/EPFL **
** **
\* */
package ch.epfl.scala
import java.nio.file.Files
import ch.epfl.scala.profiledb.{ProfileDb, ProfileDbPath}
import ch.epfl.scala.profiledb.utils.{AbsolutePath, RelativePath}
import ch.epfl.scala.profilers.ProfilingImpl
import ch.epfl.scala.profilers.tools.{Logger, ScalaSettingsOps, SettingsOps}
import scala.reflect.internal.util.{NoPosition, SourceFile, Statistics}
import scala.reflect.io.Path
import scala.tools.nsc.Reporting.WarningCategory
import scala.tools.nsc.io.AbstractFile
import scala.tools.nsc.{Global, Phase}
import scala.tools.nsc.plugins.{Plugin, PluginComponent}
import scala.util.Try
import scala.util.matching.Regex
class ProfilingPlugin(val global: Global) extends Plugin { self =>
// Every definition used at init needs to be lazy otherwise it slays the compiler
val name = "scalac-profiling"
val description = "Adds instrumentation to keep an eye on Scalac performance."
val components = List[PluginComponent](ProfilingComponent)
private final lazy val ShowProfiles = "show-profiles"
private final lazy val SourceRoot = "sourceroot"
private final lazy val CrossTarget = "cross-target"
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"
private final lazy val PrintSearchRegex = s"$PrintSearchResult:(.*)".r
private final lazy val SourceRootRegex = s"$SourceRoot:(.*)".r
private final lazy val CrossTargetRegex = s"$CrossTarget:(.*)".r
def findOption(name: String, pattern: Regex): Option[String] = {
super.options.find(_.startsWith(name)).flatMap {
case pattern(matched) => Some(matched)
case _ => None
}
}
def findSearchIds(userOption: Option[String]): Set[Int] = {
userOption match {
case Some(value) => value.split(",", Int.MaxValue).map(_.toInt).toSet
case None => Set.empty
}
}
private final lazy val config = {
val sourceRoot = findOption(SourceRoot, SourceRootRegex)
.map(AbsolutePath.apply)
.getOrElse(AbsolutePath.workingDirectory)
val crossTarget = findOption(CrossTarget, CrossTargetRegex)
.map(AbsolutePath.apply)
.getOrElse {
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"
)
sourceRoot.resolve(RelativePath(s"target/$scalaDir"))
}
PluginConfig(
showProfiles = super.options.contains(ShowProfiles),
generateDb = super.options.contains(GenerateProfileDb),
sourceRoot = sourceRoot,
crossTarget = crossTarget,
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
// format: off
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:$CrossTarget` option to manage the target directory for the resulting flamegraph file, otherwise, the SBT target directory will be picked.
|-P:$name:${pad20(GenerateMacroFlamegraph)} Generates a flamegraph for macro expansions. The flamegraph for implicit searches is enabled by default.
|-P:$name:${pad20(GenerateProfileDb)} Generates profiledb (will be removed later).
|-P:$name:${pad20(PrintFailedMacroImplicits)} Prints trees of all failed implicit searches that triggered a macro expansion.
|-P:$name:${pad20(PrintSearchResult)}:_ Print implicit search result trees for a list of search ids separated by a comma.
|-P:$name:${pad20(ShowConcreteImplicitTparams)} Shows types in flamegraphs of implicits with concrete type params.
|-P:$name:${pad20(ShowProfiles)} Logs profile information for every call-site.
|-P:$name:${pad20(SourceRoot)}:_ Sets the source root for this project.
|-P:$name:${pad20(CrossTarget)}:_ Sets the cross target for this project.
""".stripMargin
) // format: on
lazy val implementation = new ProfilingImpl(ProfilingPlugin.this.global, config, logger)
implementation.registerProfilers()
private object ProfilingComponent extends PluginComponent {
override val global: implementation.global.type = implementation.global
override val phaseName: String = "scalacenter-profiling"
override val runsAfter: List[String] = List("jvm")
override val runsBefore: List[String] = List("terminal")
private def showExpansion(expansion: (global.Tree, Int)): (String, Int) =
global.showCode(expansion._1) -> expansion._2
// This is just for displaying purposes
import scala.collection.mutable.LinkedHashMap
private def toLinkedHashMap[K, V](xs: Seq[(K, V)]): LinkedHashMap[K, V] = {
val builder = LinkedHashMap.newBuilder[K, V]
builder.++=(xs)
builder.result()
}
private def reportStatistics(graphsPath: AbsolutePath): Unit = {
val globalDir =
if (config.generateGlobalFlamegraph) {
val globalDir =
ProfileDbPath.toGraphsProfilePath(
config.crossTarget.resolve(RelativePath("classes"))
)
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)
val expansions = macroProfiler.repeatedExpansions.map(showExpansion)
logger.info("Macro repeated expansions", expansions)
val macrosType = implementation.macrosByType.toList.sortBy(_._2)
val macrosTypeLines = global.exitingTyper(macrosType.map(kv => kv._1.toString -> kv._2))
logger.info("Macro expansions by type", toLinkedHashMap(macrosTypeLines))
val implicitSearchesPosition = toLinkedHashMap(
implementation.implicitSearchesByPos.toList.sortBy(_._2)
)
logger.info("Implicit searches by position", implicitSearchesPosition)
val sortedImplicitSearches =
implementation.implicitSearchesSourceFilesByType.toVector
.flatMap {
case (tpe, sourceFiles) =>
val firings = implementation.implicitSearchesByType.getOrElse(tpe, 0)
val files = sourceFiles.toList.flatMap {
case f if f.length > 0 =>
List(f.path)
case _ =>
List.empty
}
ImplicitSearchDebugInfo(firings, files).map(tpe -> _)
}
.sortBy(_._2.firings)
// Make sure to stringify types right after typer to avoid compiler crashes
val stringifiedSortedImplicitSearches =
global.exitingTyper(
sortedImplicitSearches
.map(kv => kv._1.toString() -> kv._2)
)
logger.info("Implicit searches by type", toLinkedHashMap(stringifiedSortedImplicitSearches))
}
}
import com.google.protobuf.duration.Duration
import com.google.protobuf.timestamp.Timestamp
import ch.epfl.scala.profiledb.{profiledb => schema}
private final val nanoScale: Int = 1000000000
private def toDuration(nanos: Long): Duration = {
val seconds: Long = nanos / nanoScale
val remainder: Int = (nanos % nanoScale).toInt
Duration(seconds = seconds, nanos = remainder)
}
private lazy val getCurrentTimestamp: Timestamp = {
val duration = toDuration(System.nanoTime())
Timestamp(seconds = duration.seconds, nanos = duration.nanos)
}
private def toGlobalDatabase(statistics: Statistics): schema.Database = {
import statistics.{Timer, Counter}
def toSchemaTimer(scalacTimer: Timer): schema.Timer = {
val id = scalacTimer.prefix
val duration = toDuration(scalacTimer.nanos)
schema.Timer(id = id, duration = Some(duration))
}
def toSchemaCounter(scalacCounter: Counter): schema.Counter = {
val id = scalacCounter.prefix
val ticks = scalacCounter.value.toLong
schema.Counter(id = id, ticks = ticks)
}
val allScalacPhases = global.phaseDescriptors.map(_.phaseName)
val scalacQuantities = statistics.allQuantities.toList
val quantitiesPerPhase =
allScalacPhases.map(phase => phase -> scalacQuantities.filter(_.showAt(phase)))
val phaseProfiles = quantitiesPerPhase.map {
case (phaseName, phaseQuantities) =>
val timers = phaseQuantities.collect { case t: Timer => t }.map(toSchemaTimer)
val counters = phaseQuantities.collect { case c: Counter => c }.map(toSchemaCounter)
schema.PhaseProfile(name = phaseName, timers = timers, counters = counters)
}
val timestamp = Some(getCurrentTimestamp)
val runProfile = Some(schema.RunProfile(phaseProfiles = phaseProfiles))
val entry = schema.DatabaseEntry(
timestamp = timestamp,
runProfile = runProfile,
compilationUnitProfile = None
)
schema.Database(
`type` = schema.ContentType.GLOBAL,
entries = List(entry)
)
}
private def getOutputDirFor(absFile: AbstractFile): Path = Path {
val outputPath = global.settings.outputDirs.outputDirFor(absFile).path
if (outputPath.isEmpty) "." else outputPath
}
private def dbPathFor(sourceFile: SourceFile): Option[ProfileDbPath] = {
val absoluteSourceFile = AbsolutePath(sourceFile.file.path)
val targetPath = absoluteSourceFile.toRelative(config.sourceRoot)
if (targetPath.syntax.endsWith(".scala")) {
val outputDir = getOutputDirFor(sourceFile.file)
val absoluteOutput = AbsolutePath(outputDir.jfile)
val dbTargetPath = ProfileDbPath.toProfileDbPath(targetPath)
Some(ProfileDbPath(absoluteOutput, dbTargetPath))
} else None
}
private final val EmptyDuration = Duration.defaultInstance
private def profileDbEntryFor(sourceFile: SourceFile): schema.DatabaseEntry = {
import scala.reflect.internal.util.Position
import implementation.{MacroInfo, ImplicitInfo}
def perFile[V](ps: Map[Position, V]): Map[Position, V] =
ps.collect { case t @ (pos, _) if pos.source == sourceFile => t }
def toPos(pos: Position): schema.Position = {
val point = pos.point
val line = pos.line
val column = pos.column
schema.Position(point = point, line = line, column = column)
}
def toMacroProfile(pos: Position, info: MacroInfo): schema.MacroProfile = {
val currentPos = Some(toPos(pos))
val expandedMacros = info.expandedMacros.toLong
val approximateSize = info.expandedNodes.toLong
val duration = Some(toDuration(info.expansionNanos))
schema.MacroProfile(
position = currentPos,
expandedMacros = expandedMacros,
approximateSize = approximateSize,
duration = duration
)
}
def toImplicitProfile(pos: Position, info: ImplicitInfo): schema.ImplicitSearchProfile = {
val currentPos = Some(toPos(pos))
val searches = info.count.toLong
val duration = Some(EmptyDuration)
schema.ImplicitSearchProfile(
position = currentPos,
searches = searches,
duration = duration
)
}
val macroProfiles = perFile(implementation.macroProfiler.perCallSite)
.map { case (pos: Position, info: MacroInfo) => toMacroProfile(pos, info) }
val implicitSearchProfiles = perFile(implementation.implicitProfiler.perCallSite)
.map { case (pos: Position, info: ImplicitInfo) => toImplicitProfile(pos, info) }
val timestamp = Some(getCurrentTimestamp)
val compilationUnitProfile = Some(
schema.CompilationUnitProfile(
macroProfiles = macroProfiles.toList,
implicitSearchProfiles = implicitSearchProfiles.toList
)
)
schema.DatabaseEntry(timestamp = timestamp, compilationUnitProfile = compilationUnitProfile)
}
def writeDatabase(db: schema.Database, path: ProfileDbPath): Try[schema.Database] = {
if (Files.exists(path.target.underlying)) {
ProfileDb.read(path).flatMap { oldDb =>
val oldDbType = oldDb.`type`
val newDbType = db.`type`
if (
oldDbType.isGlobal && newDbType.isGlobal ||
(oldDbType.isPerCompilationUnit && newDbType.isPerCompilationUnit)
) {
val updatedDb = oldDb.addAllEntries(db.entries)
ProfileDb.write(updatedDb, path)
} else Try(sys.error(s"Db type mismatch: $newDbType != $oldDbType"))
}
} else ProfileDb.write(db, path)
}
lazy val globalOutputDir = AbsolutePath(
new java.io.File(
global.settings.outputDirs.getSingleOutput
.map(_.file.getAbsolutePath)
.getOrElse(global.settings.outdir.value)
)
)
private final val PerCompilationUnit = schema.ContentType.PER_COMPILATION_UNIT
override def newPhase(prev: Phase): Phase = {
new StdPhase(prev) {
override def apply(unit: global.CompilationUnit): Unit = {
if (
SettingsOps.areStatisticsEnabled(global) &&
config.generateDb
) {
val currentSourceFile = unit.source
val compilationUnitEntry = profileDbEntryFor(currentSourceFile)
dbPathFor(currentSourceFile) match {
case Some(profileDbPath) =>
val canonicalTarget = profileDbPath.target.underlying.normalize()
logger.debug(s"Creating profiledb for ${canonicalTarget}")
val freshDatabase =
schema.Database(`type` = PerCompilationUnit, entries = List(compilationUnitEntry))
writeDatabase(freshDatabase, profileDbPath).failed
.foreach(t => global.globalError(s"I/O profiledb error: ${t.getMessage}"))
case None => global.globalError(s"Could not write profiledb for $currentSourceFile.")
}
}
}
override def run(): Unit = {
super.run()
if (!SettingsOps.areStatisticsEnabled(global)) {
val flagName = global.settings.Ystatistics.name
global.runReporting.warning(
NoPosition,
s"`${self.name}` compiler plugin requires the option `$flagName` to be enabled",
WarningCategory.OtherDebug,
""
)
}
val graphsRelativePath = ProfileDbPath.GraphsProfileDbRelativePath
val graphsDir = globalOutputDir.resolve(graphsRelativePath)
reportStatistics(graphsDir)
if (config.generateDb) {
val globalDatabase = toGlobalDatabase(global.statistics)
val globalRelativePath = ProfileDbPath.GlobalProfileDbRelativePath
val globalProfileDbPath = ProfileDbPath(globalOutputDir, globalRelativePath)
val pathToWrite = globalProfileDbPath.target.underlying.toAbsolutePath
logger.info(s"Creating global statistics information at $pathToWrite.")
writeDatabase(globalDatabase, globalProfileDbPath)
}
}
}
}
}
}