Skip to content

Add -Vprofile option #15406

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

Merged
merged 7 commits into from
Jun 13, 2022
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
17 changes: 14 additions & 3 deletions compiler/src/dotty/tools/dotc/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import io.AbstractFile
import Phases.unfusedPhases

import util._
import reporting.{Suppression, Action}
import reporting.{Suppression, Action, Profile, ActiveProfile, NoProfile}
import reporting.Diagnostic
import reporting.Diagnostic.Warning
import rewrites.Rewrites
Expand Down Expand Up @@ -197,12 +197,21 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
compileUnits()(using ctx)
}

var profile: Profile = NoProfile

private def compileUnits()(using Context) = Stats.maybeMonitored {
if (!ctx.mode.is(Mode.Interactive)) // IDEs might have multi-threaded access, accesses are synchronized
ctx.base.checkSingleThreaded()

compiling = true

profile =
if ctx.settings.Vprofile.value
|| !ctx.settings.VprofileSortedBy.value.isEmpty
|| ctx.settings.VprofileDetails.value != 0
then ActiveProfile(ctx.settings.VprofileDetails.value.max(0).min(1000))
else NoProfile

// If testing pickler, make sure to stop after pickling phase:
val stopAfter =
if (ctx.settings.YtestPickler.value) List("pickler")
Expand Down Expand Up @@ -321,8 +330,10 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
def printSummary(): Unit = {
printMaxConstraint()
val r = runContext.reporter
r.summarizeUnreportedWarnings
r.printSummary
if !r.errorsReported then
profile.printSummary()
r.summarizeUnreportedWarnings()
r.printSummary()
}

override def reset(): Unit = {
Expand Down
5 changes: 5 additions & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ private sealed trait VerboseSettings:
val Xprint: Setting[List[String]] = PhasesSetting("-Vprint", "Print out program after", aliases = List("-Xprint"))
val XshowPhases: Setting[Boolean] = BooleanSetting("-Vphases", "List compiler phases.", aliases = List("-Xshow-phases"))

val Vprofile: Setting[Boolean] = BooleanSetting("-Vprofile", "Show metrics about sources and internal representations to estimate compile-time complexity.")
val VprofileSortedBy = ChoiceSetting("-Vprofile-sorted-by", "key", "Show metrics about sources and internal representations sorted by given column name", List("name", "path", "lines", "tokens", "tasty", "complexity"), "")
val VprofileDetails = IntSetting("-Vprofile-details", "Show metrics about sources and internal representations of the most complex methods", 0)

/** -W "Warnings" settings
*/
private sealed trait WarningSettings:
Expand Down Expand Up @@ -334,3 +338,4 @@ private sealed trait YSettings:

val YforceInlineWhileTyping: Setting[Boolean] = BooleanSetting("-Yforce-inline-while-typing", "Make non-transparent inline methods inline when typing. Emulates the old inlining behavior of 3.0.0-M3.")
end YSettings

7 changes: 7 additions & 0 deletions compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import StdNames.nme
import transform.SymUtils._
import config.Config
import collection.mutable
import reporting.{Profile, NoProfile}
import dotty.tools.tasty.TastyFormat.ASTsSection


Expand All @@ -43,6 +44,8 @@ class TreePickler(pickler: TastyPickler) {
*/
private val docStrings = util.EqHashMap[untpd.MemberDef, Comment]()

private var profile: Profile = NoProfile

def treeAnnots(tree: untpd.MemberDef): List[Tree] =
val ts = annotTrees.lookup(tree)
if ts == null then Nil else ts.toList
Expand Down Expand Up @@ -324,6 +327,7 @@ class TreePickler(pickler: TastyPickler) {
assert(symRefs(sym) == NoAddr, sym)
registerDef(sym)
writeByte(tag)
val addr = currentAddr
withLength {
pickleName(sym.name)
pickleParams
Expand All @@ -334,6 +338,8 @@ class TreePickler(pickler: TastyPickler) {
pickleTreeUnlessEmpty(rhs)
pickleModifiers(sym, mdef)
}
if sym.is(Method) && sym.owner.isClass then
profile.recordMethodSize(sym, currentAddr.index - addr.index, mdef.span)
for
docCtx <- ctx.docCtx
comment <- docCtx.docstring(sym)
Expand Down Expand Up @@ -769,6 +775,7 @@ class TreePickler(pickler: TastyPickler) {
// ---- main entry points ---------------------------------------

def pickle(trees: List[Tree])(using Context): Unit = {
profile = Profile.current
trees.foreach(tree => if (!tree.isEmpty) pickleTree(tree))
def missing = forwardSymRefs.keysIterator
.map(sym => i"${sym.showLocated} (line ${sym.srcPos.line}) #${sym.id}")
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ object Parsers {

class Parser(source: SourceFile)(using Context) extends ParserCommon(source) {

val in: Scanner = new Scanner(source)
val in: Scanner = new Scanner(source, profile = Profile.current)
// in.debugTokenStream = true // uncomment to see the token stream of the standard scanner, but not syntax highlighting

/** This is the general parse entry point.
Expand Down
6 changes: 5 additions & 1 deletion compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import rewrites.Rewrites.patch
import config.Feature
import config.Feature.migrateTo3
import config.SourceVersion.`3.0`
import reporting.{NoProfile, Profile}

object Scanners {

Expand Down Expand Up @@ -161,7 +162,7 @@ object Scanners {
errorButContinue("trailing separator is not allowed", offset + litBuf.length - 1)
}

class Scanner(source: SourceFile, override val startFrom: Offset = 0)(using Context) extends ScannerCommon(source) {
class Scanner(source: SourceFile, override val startFrom: Offset = 0, profile: Profile = NoProfile)(using Context) extends ScannerCommon(source) {
val keepComments = !ctx.settings.YdropComments.value

/** A switch whether operators at the start of lines can be infix operators */
Expand Down Expand Up @@ -399,6 +400,7 @@ object Scanners {
getNextToken(lastToken)
if isAfterLineEnd then handleNewLine(lastToken)
postProcessToken(lastToken, lastName)
profile.recordNewToken()
printState()

final def printState() =
Expand Down Expand Up @@ -639,6 +641,8 @@ object Scanners {
errorButContinue(spaceTabMismatchMsg(lastWidth, nextWidth))
if token != OUTDENT then
handleNewIndentWidth(currentRegion, _.otherIndentWidths += nextWidth)
if next.token == EMPTY then
profile.recordNewLine()
end handleNewLine

def spaceTabMismatchMsg(lastWidth: IndentWidth, nextWidth: IndentWidth) =
Expand Down
157 changes: 157 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/Profile.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package dotty.tools
package dotc
package reporting

import core.*
import Contexts.{Context, ctx}
import Symbols.{Symbol, NoSymbol}
import collection.mutable
import util.{EqHashMap, NoSourcePosition}
import util.Spans.{Span, NoSpan}
import Decorators.i
import parsing.Scanners.Scanner
import io.AbstractFile
import annotation.internal.sharable

abstract class Profile:
def unitProfile(unit: CompilationUnit): Profile.Info
def recordNewLine()(using Context): Unit
def recordNewToken()(using Context): Unit
def recordTasty(size: Int)(using Context): Unit
def recordMethodSize(meth: Symbol, size: Int, span: Span)(using Context): Unit
def printSummary()(using Context): Unit

object Profile:
def current(using Context): Profile =
val run = ctx.run
if run == null then NoProfile else run.profile

inline val TastyChunkSize = 50

def chunks(size: Int) = (size + TastyChunkSize - 1) / TastyChunkSize

case class MethodInfo(meth: Symbol, size: Int, span: Span)
@sharable object NoInfo extends MethodInfo(NoSymbol, 0, NoSpan)

class Info(details: Int):
var lineCount: Int = 0
var tokenCount: Int = 0
var tastySize: Int = 0
def complexity: Float = chunks(tastySize).toFloat/lineCount
val leading: Array[MethodInfo] = Array.fill[MethodInfo](details)(NoInfo)

def recordMethodSize(meth: Symbol, size: Int, span: Span): Unit =
var i = leading.length
while i > 0 && leading(i - 1).size < size do
if i < leading.length then leading(i) = leading(i - 1)
i -= 1
if i < leading.length then
leading(i) = MethodInfo(meth, size, span)
end Info
end Profile

class ActiveProfile(details: Int) extends Profile:

private val pinfo = new EqHashMap[CompilationUnit, Profile.Info]

private val junkInfo = new Profile.Info(0)

private def curInfo(using Context): Profile.Info =
val unit: CompilationUnit | Null = ctx.compilationUnit
if unit == null || unit.source.file.isVirtual then junkInfo else unitProfile(unit)

def unitProfile(unit: CompilationUnit): Profile.Info =
pinfo.getOrElseUpdate(unit, new Profile.Info(details))

def recordNewLine()(using Context): Unit =
curInfo.lineCount += 1
def recordNewToken()(using Context): Unit =
curInfo.tokenCount += 1
def recordTasty(size: Int)(using Context): Unit =
curInfo.tastySize += size
def recordMethodSize(meth: Symbol, size: Int, span: Span)(using Context): Unit =
curInfo.recordMethodSize(meth, size, span)

def printSummary()(using Context): Unit =
val units =
val rawUnits = pinfo.keysIterator.toArray
ctx.settings.VprofileSortedBy.value match
case "name" => rawUnits.sortBy(_.source.file.name)
case "path" => rawUnits.sortBy(_.source.file.path)
case "lines" => rawUnits.sortBy(unitProfile(_).lineCount)
case "tokens" => rawUnits.sortBy(unitProfile(_).tokenCount)
case "complexity" => rawUnits.sortBy(unitProfile(_).complexity)
case _ => rawUnits.sortBy(unitProfile(_).tastySize)

def printHeader(sourceNameWidth: Int, methNameWidth: Int = 0): String =
val prefix =
if methNameWidth > 0
then s"%-${sourceNameWidth}s %-${methNameWidth}s".format("Sourcefile", "Method")
else s"%-${sourceNameWidth}s".format("Sourcefile")
val layout = s"%-${prefix.length}s %6s %8s %7s %s %s"
report.echo(layout.format(prefix, "Lines", "Tokens", "Tasty", " Complexity/Line", "Directory"))
layout

def printInfo(layout: String, name: String, info: Profile.Info, path: String) =
val complexity = info.complexity
val explanation =
if complexity < 1 then "low "
else if complexity < 5 then "moderate"
else if complexity < 25 then "high "
else "extreme "
report.echo(layout.format(
name, info.lineCount, info.tokenCount, Profile.chunks(info.tastySize),
s"${"%6.2f".format(complexity)} $explanation", path))

def safeMax(xs: Array[Int]) = xs.max.max(10).min(50)

def printAndAggregateSourceInfos(): Profile.Info =
val sourceNameWidth = safeMax(units.map(_.source.file.name.length))
val layout = printHeader(sourceNameWidth)
val agg = new Profile.Info(details)
for unit <- units do
val file = unit.source.file
val info = unitProfile(unit)
printInfo(layout, file.name, info, file.container.path)
agg.lineCount += info.lineCount
agg.tokenCount += info.tokenCount
agg.tastySize += info.tastySize
for Profile.MethodInfo(meth, size, span) <- info.leading do
agg.recordMethodSize(meth, size, span)
if units.length > 1 then
report.echo(s"${"-" * sourceNameWidth}------------------------------------------")
printInfo(layout, "Total", agg, "")
agg

def printDetails(agg: Profile.Info): Unit =
val sourceNameWidth = safeMax(agg.leading.map(_.meth.source.name.length))
val methNameWidth = safeMax(agg.leading.map(_.meth.name.toString.length))
report.echo("\nMost complex methods:")
val layout = printHeader(sourceNameWidth, methNameWidth)
for
Profile.MethodInfo(meth, size, span) <- agg.leading.reverse
unit <- units.find(_.source.eq(meth.source))
do
val methProfile = new ActiveProfile(0)
val methCtx = ctx.fresh.setCompilationUnit(unit)
val s = Scanner(meth.source, span.start, methProfile)(using methCtx)
while s.offset < span.end do s.nextToken()
val info = methProfile.unitProfile(unit)
info.lineCount += 1
info.tastySize = size
val file = meth.source.file
val header = s"%-${sourceNameWidth}s %-${methNameWidth}s".format(file.name, meth.name)
printInfo(layout, header, info, file.container.path)

val agg = printAndAggregateSourceInfos()
if details > 0 then printDetails(agg)
end printSummary
end ActiveProfile

object NoProfile extends Profile:
def unitProfile(unit: CompilationUnit) = unsupported("NoProfile.info")
def recordNewLine()(using Context): Unit = ()
def recordNewToken()(using Context): Unit = ()
def recordTasty(size: Int)(using Context): Unit = ()
def recordMethodSize(meth: Symbol, size: Int, span: Span)(using Context): Unit = ()
def printSummary()(using Context): Unit = ()
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/reporting/Reporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,14 @@ abstract class Reporter extends interfaces.ReporterResult {
b.mkString("\n")
}

def summarizeUnreportedWarnings(using Context): Unit =
def summarizeUnreportedWarnings()(using Context): Unit =
for (settingName, count) <- unreportedWarnings do
val were = if count == 1 then "was" else "were"
val msg = s"there $were ${countString(count, settingName.tail + " warning")}; re-run with $settingName for details"
report(Warning(msg, NoSourcePosition))

/** Print the summary of warnings and errors */
def printSummary(using Context): Unit = {
def printSummary()(using Context): Unit = {
val s = summary
if (s != "") report(new Info(s, NoSourcePosition))
}
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/Pickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Periods._
import Phases._
import Symbols._
import Flags.Module
import reporting.ThrowingReporter
import reporting.{ThrowingReporter, Profile}
import collection.mutable
import scala.concurrent.{Future, Await, ExecutionContext}
import scala.concurrent.duration.Duration
Expand Down Expand Up @@ -70,6 +70,7 @@ class Pickler extends Phase {
picklers(cls) = pickler
val treePkl = new TreePickler(pickler)
treePkl.pickle(tree :: Nil)
Profile.current.recordTasty(treePkl.buf.length)
val positionWarnings = new mutable.ListBuffer[String]()
val pickledF = inContext(ctx.fresh) {
Future {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ object desugarPackage extends DeSugarTest {
val buf = parsedTrees map desugarTree
val ms2 = (System.nanoTime() - start)/1000000
println(s"$parsed files parsed in ${ms1}ms, ${nodes - startNodes} nodes desugared in ${ms2-ms1}ms, total trees created = ${Trees.ntrees - startNodes}")
ctx.reporter.printSummary
ctx.reporter.printSummary()
}

def main(args: Array[String]): Unit = {
Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/dotc/parsing/parsePackage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ object parsePackage extends ParserTest {
val buf = parsedTrees map transformer.transform
val ms2 = (System.nanoTime() - start)/1000000
println(s"$parsed files parsed in ${ms1}ms, $nodes nodes transformed in ${ms2-ms1}ms, total trees created = ${Trees.ntrees}")
ctx.reporter.printSummary
ctx.reporter.printSummary()
}

def main(args: Array[String]): Unit = {
Expand Down
29 changes: 29 additions & 0 deletions tests/pos/profile-test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// When compiling this with -Yprofile, this should output
//
// Source file Lines Tokens Tasty Complexity/Line Directory
// profile-test.scala 16 50 316 0.49 low tests/pos
object ProfileTest:

def test = ???

def bar: Boolean = ??? ;
def baz = ???

/** doc comment
*/
def bam = ???

if bar then
// comment
baz
else
bam

if bar then {
baz
}
else {
// comment
bam
}