Skip to content

Commit 9aca2c2

Browse files
authored
Merge pull request #15406 from dotty-staging/add-profile
Add -Vprofile option
2 parents 5332a12 + 2ece47d commit 9aca2c2

File tree

11 files changed

+224
-10
lines changed

11 files changed

+224
-10
lines changed

compiler/src/dotty/tools/dotc/Run.scala

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import io.AbstractFile
1515
import Phases.unfusedPhases
1616

1717
import util._
18-
import reporting.{Suppression, Action}
18+
import reporting.{Suppression, Action, Profile, ActiveProfile, NoProfile}
1919
import reporting.Diagnostic
2020
import reporting.Diagnostic.Warning
2121
import rewrites.Rewrites
@@ -197,12 +197,21 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
197197
compileUnits()(using ctx)
198198
}
199199

200+
var profile: Profile = NoProfile
201+
200202
private def compileUnits()(using Context) = Stats.maybeMonitored {
201203
if (!ctx.mode.is(Mode.Interactive)) // IDEs might have multi-threaded access, accesses are synchronized
202204
ctx.base.checkSingleThreaded()
203205

204206
compiling = true
205207

208+
profile =
209+
if ctx.settings.Vprofile.value
210+
|| !ctx.settings.VprofileSortedBy.value.isEmpty
211+
|| ctx.settings.VprofileDetails.value != 0
212+
then ActiveProfile(ctx.settings.VprofileDetails.value.max(0).min(1000))
213+
else NoProfile
214+
206215
// If testing pickler, make sure to stop after pickling phase:
207216
val stopAfter =
208217
if (ctx.settings.YtestPickler.value) List("pickler")
@@ -321,8 +330,10 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
321330
def printSummary(): Unit = {
322331
printMaxConstraint()
323332
val r = runContext.reporter
324-
r.summarizeUnreportedWarnings
325-
r.printSummary
333+
if !r.errorsReported then
334+
profile.printSummary()
335+
r.summarizeUnreportedWarnings()
336+
r.printSummary()
326337
}
327338

328339
override def reset(): Unit = {

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ private sealed trait VerboseSettings:
146146
val Xprint: Setting[List[String]] = PhasesSetting("-Vprint", "Print out program after", aliases = List("-Xprint"))
147147
val XshowPhases: Setting[Boolean] = BooleanSetting("-Vphases", "List compiler phases.", aliases = List("-Xshow-phases"))
148148

149+
val Vprofile: Setting[Boolean] = BooleanSetting("-Vprofile", "Show metrics about sources and internal representations to estimate compile-time complexity.")
150+
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"), "")
151+
val VprofileDetails = IntSetting("-Vprofile-details", "Show metrics about sources and internal representations of the most complex methods", 0)
152+
149153
/** -W "Warnings" settings
150154
*/
151155
private sealed trait WarningSettings:
@@ -334,3 +338,4 @@ private sealed trait YSettings:
334338

335339
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.")
336340
end YSettings
341+

compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import StdNames.nme
1717
import transform.SymUtils._
1818
import config.Config
1919
import collection.mutable
20+
import reporting.{Profile, NoProfile}
2021
import dotty.tools.tasty.TastyFormat.ASTsSection
2122

2223

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

47+
private var profile: Profile = NoProfile
48+
4649
def treeAnnots(tree: untpd.MemberDef): List[Tree] =
4750
val ts = annotTrees.lookup(tree)
4851
if ts == null then Nil else ts.toList
@@ -324,6 +327,7 @@ class TreePickler(pickler: TastyPickler) {
324327
assert(symRefs(sym) == NoAddr, sym)
325328
registerDef(sym)
326329
writeByte(tag)
330+
val addr = currentAddr
327331
withLength {
328332
pickleName(sym.name)
329333
pickleParams
@@ -334,6 +338,8 @@ class TreePickler(pickler: TastyPickler) {
334338
pickleTreeUnlessEmpty(rhs)
335339
pickleModifiers(sym, mdef)
336340
}
341+
if sym.is(Method) && sym.owner.isClass then
342+
profile.recordMethodSize(sym, currentAddr.index - addr.index, mdef.span)
337343
for
338344
docCtx <- ctx.docCtx
339345
comment <- docCtx.docstring(sym)
@@ -769,6 +775,7 @@ class TreePickler(pickler: TastyPickler) {
769775
// ---- main entry points ---------------------------------------
770776

771777
def pickle(trees: List[Tree])(using Context): Unit = {
778+
profile = Profile.current
772779
trees.foreach(tree => if (!tree.isEmpty) pickleTree(tree))
773780
def missing = forwardSymRefs.keysIterator
774781
.map(sym => i"${sym.showLocated} (line ${sym.srcPos.line}) #${sym.id}")

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ object Parsers {
172172

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

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

178178
/** This is the general parse entry point.

compiler/src/dotty/tools/dotc/parsing/Scanners.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import rewrites.Rewrites.patch
1919
import config.Feature
2020
import config.Feature.migrateTo3
2121
import config.SourceVersion.`3.0`
22+
import reporting.{NoProfile, Profile}
2223

2324
object Scanners {
2425

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

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

167168
/** A switch whether operators at the start of lines can be infix operators */
@@ -404,6 +405,7 @@ object Scanners {
404405
getNextToken(lastToken)
405406
if isAfterLineEnd then handleNewLine(lastToken)
406407
postProcessToken(lastToken, lastName)
408+
profile.recordNewToken()
407409
printState()
408410

409411
final def printState() =
@@ -644,6 +646,8 @@ object Scanners {
644646
errorButContinue(spaceTabMismatchMsg(lastWidth, nextWidth))
645647
if token != OUTDENT then
646648
handleNewIndentWidth(currentRegion, _.otherIndentWidths += nextWidth)
649+
if next.token == EMPTY then
650+
profile.recordNewLine()
647651
end handleNewLine
648652

649653
def spaceTabMismatchMsg(lastWidth: IndentWidth, nextWidth: IndentWidth) =
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package dotty.tools
2+
package dotc
3+
package reporting
4+
5+
import core.*
6+
import Contexts.{Context, ctx}
7+
import Symbols.{Symbol, NoSymbol}
8+
import collection.mutable
9+
import util.{EqHashMap, NoSourcePosition}
10+
import util.Spans.{Span, NoSpan}
11+
import Decorators.i
12+
import parsing.Scanners.Scanner
13+
import io.AbstractFile
14+
import annotation.internal.sharable
15+
16+
abstract class Profile:
17+
def unitProfile(unit: CompilationUnit): Profile.Info
18+
def recordNewLine()(using Context): Unit
19+
def recordNewToken()(using Context): Unit
20+
def recordTasty(size: Int)(using Context): Unit
21+
def recordMethodSize(meth: Symbol, size: Int, span: Span)(using Context): Unit
22+
def printSummary()(using Context): Unit
23+
24+
object Profile:
25+
def current(using Context): Profile =
26+
val run = ctx.run
27+
if run == null then NoProfile else run.profile
28+
29+
inline val TastyChunkSize = 50
30+
31+
def chunks(size: Int) = (size + TastyChunkSize - 1) / TastyChunkSize
32+
33+
case class MethodInfo(meth: Symbol, size: Int, span: Span)
34+
@sharable object NoInfo extends MethodInfo(NoSymbol, 0, NoSpan)
35+
36+
class Info(details: Int):
37+
var lineCount: Int = 0
38+
var tokenCount: Int = 0
39+
var tastySize: Int = 0
40+
def complexity: Float = chunks(tastySize).toFloat/lineCount
41+
val leading: Array[MethodInfo] = Array.fill[MethodInfo](details)(NoInfo)
42+
43+
def recordMethodSize(meth: Symbol, size: Int, span: Span): Unit =
44+
var i = leading.length
45+
while i > 0 && leading(i - 1).size < size do
46+
if i < leading.length then leading(i) = leading(i - 1)
47+
i -= 1
48+
if i < leading.length then
49+
leading(i) = MethodInfo(meth, size, span)
50+
end Info
51+
end Profile
52+
53+
class ActiveProfile(details: Int) extends Profile:
54+
55+
private val pinfo = new EqHashMap[CompilationUnit, Profile.Info]
56+
57+
private val junkInfo = new Profile.Info(0)
58+
59+
private def curInfo(using Context): Profile.Info =
60+
val unit: CompilationUnit | Null = ctx.compilationUnit
61+
if unit == null || unit.source.file.isVirtual then junkInfo else unitProfile(unit)
62+
63+
def unitProfile(unit: CompilationUnit): Profile.Info =
64+
pinfo.getOrElseUpdate(unit, new Profile.Info(details))
65+
66+
def recordNewLine()(using Context): Unit =
67+
curInfo.lineCount += 1
68+
def recordNewToken()(using Context): Unit =
69+
curInfo.tokenCount += 1
70+
def recordTasty(size: Int)(using Context): Unit =
71+
curInfo.tastySize += size
72+
def recordMethodSize(meth: Symbol, size: Int, span: Span)(using Context): Unit =
73+
curInfo.recordMethodSize(meth, size, span)
74+
75+
def printSummary()(using Context): Unit =
76+
val units =
77+
val rawUnits = pinfo.keysIterator.toArray
78+
ctx.settings.VprofileSortedBy.value match
79+
case "name" => rawUnits.sortBy(_.source.file.name)
80+
case "path" => rawUnits.sortBy(_.source.file.path)
81+
case "lines" => rawUnits.sortBy(unitProfile(_).lineCount)
82+
case "tokens" => rawUnits.sortBy(unitProfile(_).tokenCount)
83+
case "complexity" => rawUnits.sortBy(unitProfile(_).complexity)
84+
case _ => rawUnits.sortBy(unitProfile(_).tastySize)
85+
86+
def printHeader(sourceNameWidth: Int, methNameWidth: Int = 0): String =
87+
val prefix =
88+
if methNameWidth > 0
89+
then s"%-${sourceNameWidth}s %-${methNameWidth}s".format("Sourcefile", "Method")
90+
else s"%-${sourceNameWidth}s".format("Sourcefile")
91+
val layout = s"%-${prefix.length}s %6s %8s %7s %s %s"
92+
report.echo(layout.format(prefix, "Lines", "Tokens", "Tasty", " Complexity/Line", "Directory"))
93+
layout
94+
95+
def printInfo(layout: String, name: String, info: Profile.Info, path: String) =
96+
val complexity = info.complexity
97+
val explanation =
98+
if complexity < 1 then "low "
99+
else if complexity < 5 then "moderate"
100+
else if complexity < 25 then "high "
101+
else "extreme "
102+
report.echo(layout.format(
103+
name, info.lineCount, info.tokenCount, Profile.chunks(info.tastySize),
104+
s"${"%6.2f".format(complexity)} $explanation", path))
105+
106+
def safeMax(xs: Array[Int]) = xs.max.max(10).min(50)
107+
108+
def printAndAggregateSourceInfos(): Profile.Info =
109+
val sourceNameWidth = safeMax(units.map(_.source.file.name.length))
110+
val layout = printHeader(sourceNameWidth)
111+
val agg = new Profile.Info(details)
112+
for unit <- units do
113+
val file = unit.source.file
114+
val info = unitProfile(unit)
115+
printInfo(layout, file.name, info, file.container.path)
116+
agg.lineCount += info.lineCount
117+
agg.tokenCount += info.tokenCount
118+
agg.tastySize += info.tastySize
119+
for Profile.MethodInfo(meth, size, span) <- info.leading do
120+
agg.recordMethodSize(meth, size, span)
121+
if units.length > 1 then
122+
report.echo(s"${"-" * sourceNameWidth}------------------------------------------")
123+
printInfo(layout, "Total", agg, "")
124+
agg
125+
126+
def printDetails(agg: Profile.Info): Unit =
127+
val sourceNameWidth = safeMax(agg.leading.map(_.meth.source.name.length))
128+
val methNameWidth = safeMax(agg.leading.map(_.meth.name.toString.length))
129+
report.echo("\nMost complex methods:")
130+
val layout = printHeader(sourceNameWidth, methNameWidth)
131+
for
132+
Profile.MethodInfo(meth, size, span) <- agg.leading.reverse
133+
unit <- units.find(_.source.eq(meth.source))
134+
do
135+
val methProfile = new ActiveProfile(0)
136+
val methCtx = ctx.fresh.setCompilationUnit(unit)
137+
val s = Scanner(meth.source, span.start, methProfile)(using methCtx)
138+
while s.offset < span.end do s.nextToken()
139+
val info = methProfile.unitProfile(unit)
140+
info.lineCount += 1
141+
info.tastySize = size
142+
val file = meth.source.file
143+
val header = s"%-${sourceNameWidth}s %-${methNameWidth}s".format(file.name, meth.name)
144+
printInfo(layout, header, info, file.container.path)
145+
146+
val agg = printAndAggregateSourceInfos()
147+
if details > 0 then printDetails(agg)
148+
end printSummary
149+
end ActiveProfile
150+
151+
object NoProfile extends Profile:
152+
def unitProfile(unit: CompilationUnit) = unsupported("NoProfile.info")
153+
def recordNewLine()(using Context): Unit = ()
154+
def recordNewToken()(using Context): Unit = ()
155+
def recordTasty(size: Int)(using Context): Unit = ()
156+
def recordMethodSize(meth: Symbol, size: Int, span: Span)(using Context): Unit = ()
157+
def printSummary()(using Context): Unit = ()

compiler/src/dotty/tools/dotc/reporting/Reporter.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,14 +214,14 @@ abstract class Reporter extends interfaces.ReporterResult {
214214
b.mkString("\n")
215215
}
216216

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

223223
/** Print the summary of warnings and errors */
224-
def printSummary(using Context): Unit = {
224+
def printSummary()(using Context): Unit = {
225225
val s = summary
226226
if (s != "") report(new Info(s, NoSourcePosition))
227227
}

compiler/src/dotty/tools/dotc/transform/Pickler.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Periods._
1111
import Phases._
1212
import Symbols._
1313
import Flags.Module
14-
import reporting.ThrowingReporter
14+
import reporting.{ThrowingReporter, Profile}
1515
import collection.mutable
1616
import scala.concurrent.{Future, Await, ExecutionContext}
1717
import scala.concurrent.duration.Duration
@@ -70,6 +70,7 @@ class Pickler extends Phase {
7070
picklers(cls) = pickler
7171
val treePkl = new TreePickler(pickler)
7272
treePkl.pickle(tree :: Nil)
73+
Profile.current.recordTasty(treePkl.buf.length)
7374
val positionWarnings = new mutable.ListBuffer[String]()
7475
val pickledF = inContext(ctx.fresh) {
7576
Future {

compiler/test/dotty/tools/dotc/parsing/desugarPackage.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ object desugarPackage extends DeSugarTest {
1616
val buf = parsedTrees map desugarTree
1717
val ms2 = (System.nanoTime() - start)/1000000
1818
println(s"$parsed files parsed in ${ms1}ms, ${nodes - startNodes} nodes desugared in ${ms2-ms1}ms, total trees created = ${Trees.ntrees - startNodes}")
19-
ctx.reporter.printSummary
19+
ctx.reporter.printSummary()
2020
}
2121

2222
def main(args: Array[String]): Unit = {

compiler/test/dotty/tools/dotc/parsing/parsePackage.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ object parsePackage extends ParserTest {
7070
val buf = parsedTrees map transformer.transform
7171
val ms2 = (System.nanoTime() - start)/1000000
7272
println(s"$parsed files parsed in ${ms1}ms, $nodes nodes transformed in ${ms2-ms1}ms, total trees created = ${Trees.ntrees}")
73-
ctx.reporter.printSummary
73+
ctx.reporter.printSummary()
7474
}
7575

7676
def main(args: Array[String]): Unit = {

tests/pos/profile-test.scala

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// When compiling this with -Yprofile, this should output
2+
//
3+
// Source file Lines Tokens Tasty Complexity/Line Directory
4+
// profile-test.scala 16 50 316 0.49 low tests/pos
5+
object ProfileTest:
6+
7+
def test = ???
8+
9+
def bar: Boolean = ??? ;
10+
def baz = ???
11+
12+
/** doc comment
13+
*/
14+
def bam = ???
15+
16+
if bar then
17+
// comment
18+
baz
19+
else
20+
bam
21+
22+
if bar then {
23+
baz
24+
}
25+
else {
26+
// comment
27+
bam
28+
}
29+

0 commit comments

Comments
 (0)