diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index c40dcdef2ef7..8f0bc395879e 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -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 @@ -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") @@ -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 = { diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 743571c1880e..8a66b5abca8a 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -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: @@ -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 + diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index e5e25d868864..8d80d69fdfa9 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -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 @@ -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 @@ -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 @@ -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) @@ -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}") diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index bdf19ac7d013..6ced8bbe423f 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -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. diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 93ce23cbc103..994d5633ece6 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -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 { @@ -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 */ @@ -399,6 +400,7 @@ object Scanners { getNextToken(lastToken) if isAfterLineEnd then handleNewLine(lastToken) postProcessToken(lastToken, lastName) + profile.recordNewToken() printState() final def printState() = @@ -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) = diff --git a/compiler/src/dotty/tools/dotc/reporting/Profile.scala b/compiler/src/dotty/tools/dotc/reporting/Profile.scala new file mode 100644 index 000000000000..2cd67c87f386 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/reporting/Profile.scala @@ -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 = () diff --git a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala index a5d64ea11ec6..0d5acaef4960 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala @@ -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)) } diff --git a/compiler/src/dotty/tools/dotc/transform/Pickler.scala b/compiler/src/dotty/tools/dotc/transform/Pickler.scala index 579d42ce9e46..4d9b42a36fe7 100644 --- a/compiler/src/dotty/tools/dotc/transform/Pickler.scala +++ b/compiler/src/dotty/tools/dotc/transform/Pickler.scala @@ -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 @@ -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 { diff --git a/compiler/test/dotty/tools/dotc/parsing/desugarPackage.scala b/compiler/test/dotty/tools/dotc/parsing/desugarPackage.scala index 7a728f89b61c..d6a274a16516 100644 --- a/compiler/test/dotty/tools/dotc/parsing/desugarPackage.scala +++ b/compiler/test/dotty/tools/dotc/parsing/desugarPackage.scala @@ -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 = { diff --git a/compiler/test/dotty/tools/dotc/parsing/parsePackage.scala b/compiler/test/dotty/tools/dotc/parsing/parsePackage.scala index 4ea586624933..249f2ea65bc2 100644 --- a/compiler/test/dotty/tools/dotc/parsing/parsePackage.scala +++ b/compiler/test/dotty/tools/dotc/parsing/parsePackage.scala @@ -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 = { diff --git a/tests/pos/profile-test.scala b/tests/pos/profile-test.scala new file mode 100644 index 000000000000..c3d01bb3a391 --- /dev/null +++ b/tests/pos/profile-test.scala @@ -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 + } +