@@ -12,7 +12,9 @@ import typer.Typer
1212import typer .ImportInfo .withRootImports
1313import Decorators ._
1414import io .AbstractFile
15- import Phases .unfusedPhases
15+ import Phases .{unfusedPhases , Phase }
16+
17+ import sbt .interfaces .ProgressCallback
1618
1719import util ._
1820import reporting .{Suppression , Action , Profile , ActiveProfile , NoProfile }
@@ -32,6 +34,9 @@ import scala.collection.mutable
3234import scala .util .control .NonFatal
3335import scala .io .Codec
3436
37+ import Run .Progress
38+ import scala .compiletime .uninitialized
39+
3540/** A compiler run. Exports various methods to compile source files */
3641class Run (comp : Compiler , ictx : Context ) extends ImplicitRunInfo with ConstraintRunInfo {
3742
@@ -155,14 +160,51 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
155160 }
156161
157162 /** The source files of all late entered symbols, as a set */
158- private var lateFiles = mutable.Set [AbstractFile ]()
163+ private val lateFiles = mutable.Set [AbstractFile ]()
159164
160165 /** A cache for static references to packages and classes */
161166 val staticRefs = util.EqHashMap [Name , Denotation ](initialCapacity = 1024 )
162167
163168 /** Actions that need to be performed at the end of the current compilation run */
164169 private var finalizeActions = mutable.ListBuffer [() => Unit ]()
165170
171+ private var _progress : Progress | Null = null // Set if progress reporting is enabled
172+
173+ /** Only safe to call if progress is being tracked. */
174+ private inline def trackProgress (using Context )(inline op : Context ?=> Progress => Unit ): Unit =
175+ val local = _progress
176+ if local != null then
177+ op(using ctx)(local)
178+
179+ def doBeginUnit (unit : CompilationUnit )(using Context ): Unit =
180+ trackProgress : progress =>
181+ progress.informUnitStarting(unit)
182+
183+ def doAdvanceUnit ()(using Context ): Unit =
184+ trackProgress : progress =>
185+ progress.unitc += 1 // trace that we completed a unit in the current phase
186+ progress.refreshProgress()
187+
188+ def doAdvanceLate ()(using Context ): Unit =
189+ trackProgress : progress =>
190+ progress.latec += 1 // trace that we completed a late compilation
191+ progress.refreshProgress()
192+
193+ private def doEnterPhase (currentPhase : Phase )(using Context ): Unit =
194+ trackProgress : progress =>
195+ progress.enterPhase(currentPhase)
196+
197+ private def doAdvancePhase (currentPhase : Phase , wasRan : Boolean )(using Context ): Unit =
198+ trackProgress : progress =>
199+ progress.unitc = 0 // reset unit count in current phase
200+ progress.seen += 1 // trace that we've seen a phase
201+ if wasRan then
202+ // add an extra traversal now that we completed a phase
203+ progress.traversalc += 1
204+ else
205+ // no phase was ran, remove a traversal from expected total
206+ progress.runnablePhases -= 1
207+
166208 /** Will be set to true if any of the compiled compilation units contains
167209 * a pureFunctions language import.
168210 */
@@ -233,13 +275,15 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
233275 if ctx.settings.YnoDoubleBindings .value then
234276 ctx.base.checkNoDoubleBindings = true
235277
236- def runPhases (using Context ) = {
278+ def runPhases (allPhases : Array [ Phase ])( using Context ) = {
237279 var lastPrintedTree : PrintedTree = NoPrintedTree
238280 val profiler = ctx.profiler
239281 var phasesWereAdjusted = false
240282
241- for (phase <- ctx.base.allPhases)
242- if (phase.isRunnable)
283+ for phase <- allPhases do
284+ doEnterPhase(phase)
285+ val phaseWillRun = phase.isRunnable
286+ if phaseWillRun then
243287 Stats .trackTime(s " phase time ms/ $phase" ) {
244288 val start = System .currentTimeMillis
245289 val profileBefore = profiler.beforePhase(phase)
@@ -261,14 +305,21 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
261305 if ! Feature .ccEnabledSomewhere then
262306 ctx.base.unlinkPhaseAsDenotTransformer(Phases .checkCapturesPhase.prev)
263307 ctx.base.unlinkPhaseAsDenotTransformer(Phases .checkCapturesPhase)
264-
308+ end if
309+ end if
310+ end if
311+ doAdvancePhase(phase, wasRan = phaseWillRun)
312+ end for
265313 profiler.finished()
266314 }
267315
268316 val runCtx = ctx.fresh
269317 runCtx.setProfiler(Profiler ())
270318 unfusedPhases.foreach(_.initContext(runCtx))
271- runPhases(using runCtx)
319+ val fusedPhases = runCtx.base.allPhases
320+ runCtx.withProgressCallback: cb =>
321+ _progress = Progress (cb, this , fusedPhases.length)
322+ runPhases(allPhases = fusedPhases)(using runCtx)
272323 if (! ctx.reporter.hasErrors)
273324 Rewrites .writeBack()
274325 suppressions.runFinished(hasErrors = ctx.reporter.hasErrors)
@@ -294,10 +345,9 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
294345 .withRootImports
295346
296347 def process ()(using Context ) =
297- ctx.typer.lateEnterUnit(doTypeCheck =>
298- if typeCheck then
299- if compiling then finalizeActions += doTypeCheck
300- else doTypeCheck()
348+ ctx.typer.lateEnterUnit(typeCheck)(doTypeCheck =>
349+ if compiling then finalizeActions += doTypeCheck
350+ else doTypeCheck()
301351 )
302352
303353 process()(using unitCtx)
@@ -400,7 +450,66 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
400450}
401451
402452object Run {
453+
454+ /** Computes the next MegaPhase for the given phase.*/
455+ def nextMegaPhase (phase : Phase )(using Context ): Phase = phase.megaPhase.next.megaPhase
456+
457+ private class Progress (cb : ProgressCallback , private val run : Run , val initialPhases : Int ):
458+ private [Run ] var runnablePhases : Int = initialPhases // track how many phases we expect to run
459+ private [Run ] var unitc : Int = 0 // current unit count in the current phase
460+ private [Run ] var latec : Int = 0 // current late unit count
461+ private [Run ] var traversalc : Int = 0 // completed traversals over all files
462+ private [Run ] var seen : Int = 0 // how many phases we've seen so far
463+
464+ private var currPhase : Phase = uninitialized // initialized by enterPhase
465+ private var currPhaseName : String = uninitialized // initialized by enterPhase
466+ private var nextPhaseName : String = uninitialized // initialized by enterPhase
467+
468+ private def phaseNameFor (phase : Phase ): String =
469+ if phase.exists then phase.phaseName
470+ else " <end>"
471+
472+ private [Run ] def enterPhase (newPhase : Phase )(using Context ): Unit =
473+ if newPhase ne currPhase then
474+ currPhase = newPhase
475+ currPhaseName = phaseNameFor(newPhase)
476+ nextPhaseName = phaseNameFor(Run .nextMegaPhase(newPhase))
477+ if seen > 0 then
478+ refreshProgress()
479+
480+
481+ /** Counts the number of completed full traversals over files, plus the number of units in the current phase */
482+ private def currentProgress ()(using Context ): Int =
483+ traversalc * run.files.size + unitc + latec
484+
485+ /** Total progress is computed as the sum of
486+ * - the number of traversals we expect to make over all files
487+ * - the number of late compilations
488+ */
489+ private def totalProgress ()(using Context ): Int =
490+ runnablePhases * run.files.size + run.lateFiles.size
491+
492+ private def requireInitialized (): Unit =
493+ require((currPhase : Phase | Null ) != null , " enterPhase was not called" )
494+
495+ private [Run ] def informUnitStarting (unit : CompilationUnit )(using Context ): Unit =
496+ requireInitialized()
497+ cb.informUnitStarting(currPhaseName, unit)
498+
499+ private [Run ] def refreshProgress ()(using Context ): Unit =
500+ requireInitialized()
501+ cb.progress(currentProgress(), totalProgress(), currPhaseName, nextPhaseName)
502+
403503 extension (run : Run | Null )
504+ def beginUnit (unit : CompilationUnit )(using Context ): Unit =
505+ if run != null then run.doBeginUnit(unit)
506+
507+ def advanceUnit ()(using Context ): Unit =
508+ if run != null then run.doAdvanceUnit()
509+
510+ def advanceLate ()(using Context ): Unit =
511+ if run != null then run.doAdvanceLate()
512+
404513 def enrichedErrorMessage : Boolean = if run == null then false else run.myEnrichedErrorMessage
405514 def enrichErrorMessage (errorMessage : String )(using Context ): String =
406515 if run == null then
0 commit comments