Skip to content

Commit 39ad28f

Browse files
committed
wip: 2 pass parallel compile rebootstrap post -Yjava-tasty
1 parent 6e471d1 commit 39ad28f

File tree

42 files changed

+706
-160
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+706
-160
lines changed

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

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import StdNames.nme
1818
import scala.annotation.internal.sharable
1919
import scala.util.control.NoStackTrace
2020
import transform.MacroAnnotations
21+
import dotty.tools.dotc.interfaces.AbstractFile
22+
import dotty.tools.io.NoAbstractFile
2123

2224
class CompilationUnit protected (val source: SourceFile, val info: CompilationUnitInfo | Null) {
2325

@@ -45,6 +47,7 @@ class CompilationUnit protected (val source: SourceFile, val info: CompilationUn
4547

4648
/** Pickled TASTY binaries, indexed by class. */
4749
var pickled: Map[ClassSymbol, () => Array[Byte]] = Map()
50+
var outlinePickled: Map[ClassSymbol, () => Array[Byte]] = Map()
4851

4952
/** The fresh name creator for the current unit.
5053
* FIXME(#7661): This is not fine-grained enough to enable reproducible builds,
@@ -106,6 +109,8 @@ class CompilationUnit protected (val source: SourceFile, val info: CompilationUn
106109
ctx.run.nn.suspendedUnits += this
107110
if ctx.phase == Phases.inliningPhase then
108111
suspendedAtInliningPhase = true
112+
else if ctx.settings.YearlyTastyOutput.value != NoAbstractFile then
113+
report.error(i"Compilation units may not be suspended before inlining with -Ypickle-write")
109114
throw CompilationUnit.SuspendException()
110115

111116
private var myAssignmentSpans: Map[Int, List[Span]] | Null = null

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

+9-8
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,21 @@ class Compiler {
4040
List(new sbt.ExtractDependencies) :: // Sends information on classes' dependencies to sbt via callbacks
4141
List(new semanticdb.ExtractSemanticDB.ExtractSemanticInfo) :: // Extract info into .semanticdb files
4242
List(new PostTyper) :: // Additional checks and cleanups after type checking
43+
// List(new sbt.ExtractAPI.Outline) :: // [runs in outline] Sends a representation of the API of classes to sbt via callbacks
4344
List(new sjs.PrepJSInterop) :: // Additional checks and transformations for Scala.js (Scala.js only)
4445
List(new SetRootTree) :: // Set the `rootTreeOrProvider` on class symbols
4546
Nil
4647

4748
/** Phases dealing with TASTY tree pickling and unpickling */
4849
protected def picklerPhases: List[List[Phase]] =
49-
List(new Pickler) :: // Generate TASTY info
50-
List(new sbt.ExtractAPI) :: // Sends a representation of the API of classes to sbt via callbacks
51-
List(new Inlining) :: // Inline and execute macros
52-
List(new PostInlining) :: // Add mirror support for inlined code
53-
List(new CheckUnused.PostInlining) :: // Check for unused elements
54-
List(new Staging) :: // Check staging levels and heal staged types
55-
List(new Splicing) :: // Replace level 1 splices with holes
56-
List(new PickleQuotes) :: // Turn quoted trees into explicit run-time data structures
50+
List(new Pickler) :: // Generate TASTY info
51+
List(new sbt.ExtractAPI) :: // [runs when not outline] Sends a representation of the API of classes to sbt via callbacks
52+
List(new Inlining) :: // Inline and execute macros
53+
List(new PostInlining) :: // Add mirror support for inlined code
54+
List(new CheckUnused.PostInlining) :: // Check for unused elements
55+
List(new Staging) :: // Check staging levels and heal staged types
56+
List(new Splicing) :: // Replace level 1 splices with holes
57+
List(new PickleQuotes) :: // Turn quoted trees into explicit run-time data structures
5758
Nil
5859

5960
/** Phases dealing with the transformation from pickled trees to backend trees */

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

+200-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,28 @@ import config.Feature
1313

1414
import scala.util.control.NonFatal
1515
import fromtasty.{TASTYCompiler, TastyFileUtil}
16+
import dotty.tools.io.NoAbstractFile
17+
import dotty.tools.io.{VirtualFile, VirtualDirectory}
18+
19+
import java.nio.file.Path as JPath
20+
import scala.concurrent.*
21+
import scala.annotation.internal.sharable
22+
import scala.concurrent.duration.*
23+
import scala.util.{Success, Failure}
24+
import scala.annotation.targetName
25+
import dotty.tools.dotc.classpath.FileUtils.hasScalaExtension
26+
import dotty.tools.dotc.core.Symbols
27+
28+
object Driver {
29+
@sharable lazy val executor =
30+
// TODO: systemParallelism may change over time - is it possible to update the pool size?
31+
val pool = java.util.concurrent.Executors.newFixedThreadPool(systemParallelism()).nn
32+
sys.addShutdownHook(pool.shutdown())
33+
ExecutionContext.fromExecutor(pool)
34+
35+
/** 1 less than the system's own processor count (minimum 1) */
36+
def systemParallelism() = math.max(1, Runtime.getRuntime().nn.availableProcessors() - 1)
37+
}
1638

1739
/** Run the Dotty compiler.
1840
*
@@ -28,6 +50,183 @@ class Driver {
2850

2951
protected def emptyReporter: Reporter = new StoreReporter(null)
3052

53+
protected def doCompile(files: List[AbstractFile])(using ictx: Context): Reporter =
54+
val isOutline = ictx.settings.Youtline.value(using ictx)
55+
56+
if !isOutline then inContext(ictx):
57+
report.echo(s"basic compilation enabled on files ${files.headOption.map(f => s"$f...").getOrElse("[]")}")(using ictx)
58+
doCompile(newCompiler, files) // standard compilation
59+
else
60+
report.echo(s"Outline compilation enabled on files ${files.headOption.map(f => s"$f...").getOrElse("[]")}")(using ictx)
61+
val maxParallelism = ictx.settings.YmaxParallelism.valueIn(ictx.settingsState)
62+
val absParallelism = math.abs(maxParallelism)
63+
val isParallel = maxParallelism >= 0
64+
val parallelism =
65+
val ceiling = Driver.systemParallelism()
66+
if absParallelism > 0 then math.min(absParallelism, ceiling)
67+
else ceiling
68+
69+
// NOTE: sbt will delete this potentially as soon as you call `apiPhaseCompleted`
70+
val pickleWriteOutput = ictx.settings.YearlyTastyOutput.valueIn(ictx.settingsState)
71+
val profileDestination = ictx.settings.YprofileDestination.valueIn(ictx.settingsState)
72+
73+
if pickleWriteOutput == NoAbstractFile then
74+
report.error("Requested outline compilation with `-Yexperimental-outline` " +
75+
"but did not provide output directory for TASTY files (missing `-Yearly-tasty-output` flag).")(using ictx)
76+
return ictx.reporter
77+
78+
val pickleWriteSource =
79+
pickleWriteOutput.underlyingSource match
80+
case Some(source) =>
81+
source.file.asInstanceOf[java.io.File | Null] match
82+
case f: java.io.File => Some(source)
83+
case null =>
84+
report.warning(s"Could not resolve file of ${source} (of class ${source.getClass.getName})")
85+
None
86+
case None =>
87+
if pickleWriteOutput.isInstanceOf[dotty.tools.io.JarArchive] then
88+
report.warning(s"Could not resolve underlying source of jar ${pickleWriteOutput} (of class ${pickleWriteOutput.getClass.getName})")
89+
None
90+
else
91+
report.warning(s"Could not resolve underlying source of ${pickleWriteOutput} (of class ${pickleWriteOutput.getClass.getName})")
92+
Some(pickleWriteOutput)
93+
94+
val outlineOutput = new VirtualDirectory("<outline-classpath>") {
95+
override def underlyingSource: Option[AbstractFile] = pickleWriteSource
96+
}
97+
98+
val firstPassCtx = ictx.fresh
99+
.setSetting(ictx.settings.YoutlineClasspath, outlineOutput)
100+
inContext(firstPassCtx):
101+
doCompile(newCompiler, files)
102+
103+
def secondPassCtx(id: Int, group: List[AbstractFile], promise: scala.concurrent.Promise[Unit]): Context =
104+
val profileDestination0 =
105+
if profileDestination.nonEmpty then
106+
val ext = dotty.tools.io.Path.fileExtension(profileDestination)
107+
val filename = dotty.tools.io.Path.fileName(profileDestination)
108+
s"$filename-worker-$id${if ext.isEmpty then "" else s".$ext"}"
109+
else profileDestination
110+
111+
val baseCtx = initCtx.fresh
112+
.setSettings(ictx.settingsState) // copy over the classpath arguments also
113+
.setSetting(ictx.settings.YsecondPass, true)
114+
.setSetting(ictx.settings.YoutlineClasspath, outlineOutput)
115+
.setCallbacks(ictx.store)
116+
.setDepsFinishPromise(promise)
117+
.setReporter(if isParallel then new StoreReporter(ictx.reporter) else ictx.reporter)
118+
119+
if profileDestination0.nonEmpty then
120+
baseCtx.setSetting(ictx.settings.YprofileDestination, profileDestination0)
121+
122+
// if ictx.settings.YoutlineClasspath.valueIn(ictx.settingsState).isEmpty then
123+
// baseCtx.setSetting(baseCtx.settings.YoutlineClasspath, pickleWriteAsClasspath)
124+
val fileNames: Array[String] =
125+
if sourcesRequired then group.map(_.toString).toArray else Array.empty
126+
setup(fileNames, baseCtx) match
127+
case Some((_, ctx)) =>
128+
assert(ctx.incCallback != null, s"cannot run outline without incremental callback")
129+
assert(ctx.depsFinishPromiseOpt.isDefined, s"cannot run outline without dependencies promise")
130+
ctx
131+
case None => baseCtx
132+
end secondPassCtx
133+
134+
val scalaFiles = files.filter(_.hasScalaExtension)
135+
136+
// 516 units, 8 cores => maxGroupSize = 65, unitGroups = 8, compilers = 8
137+
if !firstPassCtx.reporter.hasErrors && scalaFiles.nonEmpty then
138+
val maxGroupSize = Math.ceil(scalaFiles.length.toDouble / parallelism).toInt
139+
val fileGroups = scalaFiles.grouped(maxGroupSize).toList
140+
val compilers = fileGroups.length
141+
142+
143+
144+
def userRequestedSingleGroup = maxParallelism == 1
145+
146+
// TODO: probably not good to warn here because maybe compile is incremental
147+
// if compilers == 1 && !userRequestedSingleGroup then
148+
// val knownParallelism = maxParallelism > 0
149+
// val requestedParallelism = s"Requested parallelism with `-Ymax-parallelism` was ${maxParallelism}"
150+
// val computedAddedum =
151+
// if knownParallelism then "."
152+
// else s""",
153+
// | therefore operating with computed parallelism of ${parallelism}.""".stripMargin
154+
// val message =
155+
// s"""Outline compilation second pass will run with a single compile group.
156+
// | ${requestedParallelism}$computedAddedum
157+
// | With ${scalaUnits.length} units to compile I can only batch them into a single group.
158+
// | This will increase build times.
159+
// | Perhaps consider turning off -Youtline for this project.""".stripMargin
160+
// report.warning(message)(using firstPassCtx)
161+
162+
val promises = fileGroups.map(_ => scala.concurrent.Promise[Unit]())
163+
164+
locally:
165+
import scala.concurrent.ExecutionContext.Implicits.global
166+
Future.sequence(promises.map(_.future)).andThen {
167+
case Success(_) =>
168+
ictx.withIncCallback(_.dependencyPhaseCompleted())
169+
case Failure(ex) =>
170+
ex.printStackTrace()
171+
report.error(s"Exception during parallel compilation: ${ex.getMessage}")(using firstPassCtx)
172+
}
173+
174+
report.echo(s"Compiling $compilers groups of files ${if isParallel then "in parallel" else "sequentially"}")(using firstPassCtx)
175+
176+
def compileEager(
177+
id: Int,
178+
promise: Promise[Unit],
179+
fileGroup: List[AbstractFile]
180+
): Reporter = {
181+
if ctx.settings.verbose.value then
182+
report.echo("#Compiling: " + fileGroup.take(3).mkString("", ", ", "..."))
183+
val secondCtx = secondPassCtx(id, fileGroup, promise)
184+
val reporter = inContext(secondCtx):
185+
doCompile(newCompiler, fileGroup) // second pass
186+
if !secondCtx.reporter.hasErrors then
187+
assert(promise.isCompleted, s"promise was not completed")
188+
if ctx.settings.verbose.value then
189+
report.echo("#Done: " + fileGroup.mkString(" "))
190+
reporter
191+
}
192+
193+
def compileFuture(
194+
id: Int,
195+
promise: Promise[Unit],
196+
fileGroup: List[AbstractFile]
197+
)(using ExecutionContext): Future[Reporter] =
198+
Future {
199+
// println("#Compiling: " + fileGroup.mkString(" "))
200+
val secondCtx = secondPassCtx(id, fileGroup, promise)
201+
val reporter = inContext(secondCtx):
202+
doCompile(newCompiler, fileGroup) // second pass
203+
// println("#Done: " + fileGroup.mkString(" "))
204+
reporter
205+
}
206+
207+
def fileGroupIds = LazyList.iterate(0)(_ + 1).take(compilers)
208+
def taggedGroups = fileGroupIds.lazyZip(promises).lazyZip(fileGroups)
209+
210+
if isParallel then
211+
// val executor = java.util.concurrent.Executors.newFixedThreadPool(compilers).nn
212+
given ec: ExecutionContext = Driver.executor // ExecutionContext.fromExecutor(executor)
213+
val futureReporters = Future.sequence(taggedGroups.map(compileFuture)).andThen {
214+
case Success(reporters) =>
215+
reporters.foreach(_.flush()(using firstPassCtx))
216+
case Failure(ex) =>
217+
ex.printStackTrace
218+
report.error(s"Exception during parallel compilation: ${ex.getMessage}")(using firstPassCtx)
219+
}
220+
Await.ready(futureReporters, Duration.Inf)
221+
// executor.shutdown()
222+
else
223+
taggedGroups.map(compileEager)
224+
firstPassCtx.reporter
225+
else
226+
ictx.withIncCallback(_.dependencyPhaseCompleted()) // may be just java files compiled
227+
firstPassCtx.reporter
228+
end doCompile
229+
31230
protected def doCompile(compiler: Compiler, files: List[AbstractFile])(using Context): Reporter =
32231
if files.nonEmpty then
33232
var runOrNull = ctx.run
@@ -193,7 +392,7 @@ class Driver {
193392
def process(args: Array[String], rootCtx: Context): Reporter = {
194393
setup(args, rootCtx) match
195394
case Some((files, compileCtx)) =>
196-
doCompile(newCompiler(using compileCtx), files)(using compileCtx)
395+
doCompile(files)(using compileCtx)
197396
case None =>
198397
rootCtx.reporter
199398
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,10 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
287287
then ActiveProfile(ctx.settings.VprofileDetails.value.max(0).min(1000))
288288
else NoProfile
289289

290-
// If testing pickler, make sure to stop after pickling phase:
290+
// If testing pickler, of outline first pass, make sure to stop after pickling phase:
291291
val stopAfter =
292292
if (ctx.settings.YtestPickler.value) List("pickler")
293+
else if (ctx.isOutlineFirstPass) List("sbt-api")
293294
else ctx.settings.YstopAfter.value
294295

295296
val pluginPlan = ctx.base.addPluginPhases(ctx.base.phasePlan)

compiler/src/dotty/tools/dotc/ast/tpd.scala

+11
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,17 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
13811381
def runtimeCall(name: TermName, args: List[Tree])(using Context): Tree =
13821382
Ident(defn.ScalaRuntimeModule.requiredMethod(name).termRef).appliedToTermArgs(args)
13831383

1384+
object ElidedTree:
1385+
def from(tree: Tree)(using Context): Tree =
1386+
Typed(tree, TypeTree(AnnotatedType(tree.tpe, Annotation(defn.ElidedTreeAnnot, tree.span))))
1387+
1388+
def isElided(tree: Tree)(using Context): Boolean = unapply(tree)
1389+
1390+
def unapply(tree: Tree)(using Context): Boolean = tree match
1391+
case Typed(_, tpt) => tpt.tpe.hasAnnotation(defn.ElidedTreeAnnot)
1392+
case _ => false
1393+
1394+
13841395
/** An extractor that pulls out type arguments */
13851396
object MaybePoly:
13861397
def unapply(tree: Tree): Option[(Tree, List[Tree])] = tree match

compiler/src/dotty/tools/dotc/classpath/FileUtils.scala

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ object FileUtils {
2323

2424
def hasTastyExtension: Boolean = file.ext.isTasty
2525

26+
def hasScalaExtension: Boolean = file.ext.isScala
27+
2628
def isTasty: Boolean = !file.isDirectory && hasTastyExtension
2729

2830
def isScalaBinary: Boolean = file.isClass || file.isTasty

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

+19-9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import PartialFunction.condOpt
1212
import core.Contexts.*
1313
import Settings.*
1414
import dotty.tools.io.File
15+
import dotty.tools.io.NoAbstractFile
16+
import dotty.tools.dotc.classpath.FileUtils.isJarOrZip
1517

1618
object PathResolver {
1719

@@ -208,21 +210,28 @@ class PathResolver(using c: Context) {
208210
if (!settings.classpath.isDefault) settings.classpath.value
209211
else sys.env.getOrElse("CLASSPATH", ".")
210212

213+
def outlineClasspath: List[dotty.tools.io.AbstractFile] =
214+
if c.isOutlineFirstPass then Nil
215+
else c.settings.YoutlineClasspath.value match
216+
case NoAbstractFile => Nil
217+
case file => file :: Nil
218+
211219
import classPathFactory.*
212220

213221
// Assemble the elements!
214222
def basis: List[Iterable[ClassPath]] =
215223
val release = Option(ctx.settings.javaOutputVersion.value).filter(_.nonEmpty)
216224

217225
List(
218-
JrtClassPath(release), // 1. The Java 9+ classpath (backed by the jrt:/ virtual system, if available)
219-
classesInPath(javaBootClassPath), // 2. The Java bootstrap class path.
220-
contentsOfDirsInPath(javaExtDirs), // 3. The Java extension class path.
221-
classesInExpandedPath(javaUserClassPath), // 4. The Java application class path.
222-
classesInPath(scalaBootClassPath), // 5. The Scala boot class path.
223-
contentsOfDirsInPath(scalaExtDirs), // 6. The Scala extension class path.
224-
classesInExpandedPath(userClassPath), // 7. The Scala application class path.
225-
sourcesInPath(sourcePath) // 8. The Scala source path.
226+
outlineClasspath.map(newClassPath), // 1. The outline classpath (backed by the -Youtline-classpath option)
227+
JrtClassPath(release), // 2. The Java 9+ classpath (backed by the jrt:/ virtual system, if available)
228+
classesInPath(javaBootClassPath), // 3. The Java bootstrap class path.
229+
contentsOfDirsInPath(javaExtDirs), // 4. The Java extension class path.
230+
classesInExpandedPath(javaUserClassPath), // 5. The Java application class path.
231+
classesInPath(scalaBootClassPath), // 6. The Scala boot class path.
232+
contentsOfDirsInPath(scalaExtDirs), // 7. The Scala extension class path.
233+
classesInExpandedPath(userClassPath), // 8. The Scala application class path.
234+
sourcesInPath(sourcePath) // 9. The Scala source path.
226235
)
227236

228237
lazy val containers: List[ClassPath] = basis.flatten.distinct
@@ -238,12 +247,13 @@ class PathResolver(using c: Context) {
238247
| scalaExtDirs = %s
239248
| userClassPath = %s
240249
| sourcePath = %s
250+
| outlineClasspath = %s
241251
|}""".trim.stripMargin.format(
242252
scalaHome,
243253
ppcp(javaBootClassPath), ppcp(javaExtDirs), ppcp(javaUserClassPath),
244254
useJavaClassPath,
245255
ppcp(scalaBootClassPath), ppcp(scalaExtDirs), ppcp(userClassPath),
246-
ppcp(sourcePath)
256+
ppcp(sourcePath), outlineClasspath.map("\n" + _).mkString
247257
)
248258
}
249259

0 commit comments

Comments
 (0)