@@ -13,6 +13,28 @@ import config.Feature
13
13
14
14
import scala .util .control .NonFatal
15
15
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
+ }
16
38
17
39
/** Run the Dotty compiler.
18
40
*
@@ -28,6 +50,183 @@ class Driver {
28
50
29
51
protected def emptyReporter : Reporter = new StoreReporter (null )
30
52
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
+
31
230
protected def doCompile (compiler : Compiler , files : List [AbstractFile ])(using Context ): Reporter =
32
231
if files.nonEmpty then
33
232
var runOrNull = ctx.run
@@ -193,7 +392,7 @@ class Driver {
193
392
def process (args : Array [String ], rootCtx : Context ): Reporter = {
194
393
setup(args, rootCtx) match
195
394
case Some ((files, compileCtx)) =>
196
- doCompile(newCompiler( using compileCtx), files)(using compileCtx)
395
+ doCompile(files)(using compileCtx)
197
396
case None =>
198
397
rootCtx.reporter
199
398
}
0 commit comments