Skip to content

Commit a1a2c10

Browse files
committed
write pipelined tasty in parallel.
1 parent 926e6a3 commit a1a2c10

File tree

7 files changed

+272
-79
lines changed

7 files changed

+272
-79
lines changed

compiler/src/dotty/tools/backend/jvm/GenBCode.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import Symbols.*
1010
import dotty.tools.io.*
1111
import scala.collection.mutable
1212
import scala.compiletime.uninitialized
13+
import java.util.concurrent.TimeoutException
14+
15+
import scala.concurrent.duration.given
16+
import scala.concurrent.Await
1317

1418
class GenBCode extends Phase { self =>
1519

@@ -90,6 +94,20 @@ class GenBCode extends Phase { self =>
9094
try
9195
val result = super.runOn(units)
9296
generatedClassHandler.complete()
97+
for holder <- ctx.asyncTastyPromise do
98+
try
99+
val asyncState = Await.result(holder.promise.future, 5.seconds)
100+
for reporter <- asyncState.pending do
101+
reporter.relayReports(frontendAccess.backendReporting)
102+
catch
103+
case _: TimeoutException =>
104+
report.error(
105+
"""Timeout (5s) in backend while waiting for async writing of TASTy files to -Yearly-tasty-output,
106+
| this may be a bug in the compiler.
107+
|
108+
|Alternatively consider turning off pipelining for this project.""".stripMargin
109+
)
110+
end for
93111
result
94112
finally
95113
// frontendAccess and postProcessor are created lazilly, clean them up only if they were initialized

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ class Driver {
6666

6767
protected def command: CompilerCommand = ScalacCommand
6868

69+
private def setupAsyncTasty(ictx: FreshContext): Unit = inContext(ictx):
70+
ictx.settings.YearlyTastyOutput.value match
71+
case earlyOut if earlyOut.isDirectory && earlyOut.exists =>
72+
ictx.setInitialAsyncTasty()
73+
case _ =>
74+
() // do nothing
75+
6976
/** Setup context with initialized settings from CLI arguments, then check if there are any settings that
7077
* would change the default behaviour of the compiler.
7178
*
@@ -82,6 +89,7 @@ class Driver {
8289
Positioned.init(using ictx)
8390

8491
inContext(ictx) {
92+
setupAsyncTasty(ictx)
8593
if !ctx.settings.YdropComments.value || ctx.settings.YreadComments.value then
8694
ictx.setProperty(ContextDoc, new ContextDocstrings)
8795
val fileNamesOrNone = command.checkUsage(summary, sourcesRequired)(using ctx.settings)(using ctx.settingsState)

compiler/src/dotty/tools/dotc/core/Contexts.scala

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ import StdNames.nme
3131
import compiletime.uninitialized
3232

3333
import scala.annotation.internal.sharable
34+
import scala.concurrent.Promise
3435

3536
import DenotTransformers.DenotTransformer
3637
import dotty.tools.dotc.profile.Profiler
38+
import dotty.tools.dotc.transform.Pickler.AsyncTastyHolder
3739
import dotty.tools.dotc.sbt.interfaces.{IncrementalCallback, ProgressCallback}
3840
import util.Property.Key
3941
import util.Store
@@ -54,8 +56,9 @@ object Contexts {
5456
private val (importInfoLoc, store9) = store8.newLocation[ImportInfo | Null]()
5557
private val (typeAssignerLoc, store10) = store9.newLocation[TypeAssigner](TypeAssigner)
5658
private val (progressCallbackLoc, store11) = store10.newLocation[ProgressCallback | Null]()
59+
private val (tastyPromiseLoc, store12) = store11.newLocation[Option[AsyncTastyHolder]](None)
5760

58-
private val initialStore = store11
61+
private val initialStore = store12
5962

6063
/** The current context */
6164
inline def ctx(using ctx: Context): Context = ctx
@@ -197,6 +200,8 @@ object Contexts {
197200
/** The current settings values */
198201
def settingsState: SettingsState = store(settingsStateLoc)
199202

203+
def asyncTastyPromise: Option[AsyncTastyHolder] = store(tastyPromiseLoc)
204+
200205
/** The current compilation unit */
201206
def compilationUnit: CompilationUnit = store(compilationUnitLoc)
202207

@@ -685,6 +690,10 @@ object Contexts {
685690
updateStore(compilationUnitLoc, compilationUnit)
686691
}
687692

693+
def setInitialAsyncTasty(): this.type =
694+
assert(store(tastyPromiseLoc) == None, "trying to set async tasty promise twice!")
695+
updateStore(tastyPromiseLoc, Some(AsyncTastyHolder(settings.YearlyTastyOutput.value, Promise())))
696+
688697
def setCompilerCallback(callback: CompilerCallback): this.type = updateStore(compilerCallbackLoc, callback)
689698
def setIncCallback(callback: IncrementalCallback): this.type = updateStore(incCallbackLoc, callback)
690699
def setProgressCallback(callback: ProgressCallback): this.type = updateStore(progressCallbackLoc, callback)

compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala

Lines changed: 8 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,13 @@ class ExtractAPI extends Phase {
7070

7171
override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] =
7272
val doZincCallback = ctx.runZincPhases
73-
val sigWriter: Option[Pickler.EarlyFileWriter] = ctx.settings.YearlyTastyOutput.value match
74-
case earlyOut if earlyOut.isDirectory && earlyOut.exists =>
75-
Some(Pickler.EarlyFileWriter(earlyOut))
76-
case _ =>
77-
None
7873
val nonLocalClassSymbols = new mutable.HashSet[Symbol]
7974
val units0 =
8075
if doZincCallback then
8176
val ctx0 = ctx.withProperty(NonLocalClassSymbolsInCurrentUnits, Some(nonLocalClassSymbols))
8277
super.runOn(units)(using ctx0)
8378
else
8479
units // still run the phase for the side effects (writing TASTy files to -Yearly-tasty-output)
85-
sigWriter.foreach(writeSigFiles(units0, _))
8680
if doZincCallback then
8781
ctx.withIncCallback(recordNonLocalClasses(nonLocalClassSymbols, _))
8882
if ctx.settings.YjavaTasty.value then
@@ -91,57 +85,19 @@ class ExtractAPI extends Phase {
9185
units0
9286
end runOn
9387

94-
// Why we only write to early output in the first run?
95-
// ===================================================
96-
// TL;DR the point of pipeline compilation is to start downstream projects early,
97-
// so we don't want to wait for suspended units to be compiled.
98-
//
99-
// But why is it safe to ignore suspended units?
100-
// If this project contains a transparent macro that is called in the same project,
101-
// the compilation unit of that call will be suspended (if the macro implementation
102-
// is also in this project), causing a second run.
103-
// However before we do that run, we will have already requested sbt to begin
104-
// early downstream compilation. This means that the suspended definitions will not
105-
// be visible in *early* downstream compilation.
106-
//
107-
// However, sbt will by default prevent downstream compilation happening in this scenario,
108-
// due to the existence of macro definitions. So we are protected from failure if user tries
109-
// to use the suspended definitions.
110-
//
111-
// Additionally, it is recommended for the user to move macro implementations to another project
112-
// if they want to force early output. In this scenario the suspensions will no longer occur, so now
113-
// they will become visible in the early-output.
114-
//
115-
// See `sbt-test/pipelining/pipelining-scala-macro` and `sbt-test/pipelining/pipelining-scala-macro-force`
116-
// for examples of this in action.
117-
//
118-
// Therefore we only need to write to early output in the first run. We also provide the option
119-
// to diagnose suspensions with the `-Yno-suspended-units` flag.
120-
private def writeSigFiles(units: List[CompilationUnit], writer: Pickler.EarlyFileWriter)(using Context): Unit = {
121-
try
122-
for
123-
unit <- units
124-
(cls, pickled) <- unit.pickled
125-
if cls.isDefinedInCurrentRun
126-
do
127-
val internalName =
128-
if cls.is(Module) then cls.binaryClassName.stripSuffix(str.MODULE_SUFFIX).nn
129-
else cls.binaryClassName
130-
val _ = writer.writeTasty(internalName, pickled())
131-
finally
132-
writer.close()
133-
if ctx.settings.verbose.value then
134-
report.echo("[sig files written]")
135-
end try
136-
}
137-
13888
private def recordNonLocalClasses(nonLocalClassSymbols: mutable.HashSet[Symbol], cb: interfaces.IncrementalCallback)(using Context): Unit =
13989
for cls <- nonLocalClassSymbols do
14090
val sourceFile = cls.source
14191
if sourceFile.exists && cls.isDefinedInCurrentRun then
14292
recordNonLocalClass(cls, sourceFile, cb)
143-
cb.apiPhaseCompleted()
144-
cb.dependencyPhaseCompleted()
93+
for holder <- ctx.asyncTastyPromise do
94+
import scala.concurrent.ExecutionContext.Implicits.global
95+
// do not expect to be completed with failure
96+
holder.promise.future.foreach: state =>
97+
if !state.hasErrors then
98+
// We also await the promise at GenBCode to emit warnings/errors
99+
cb.apiPhaseCompleted()
100+
cb.dependencyPhaseCompleted()
145101

146102
private def recordNonLocalClass(cls: Symbol, sourceFile: SourceFile, cb: interfaces.IncrementalCallback)(using Context): Unit =
147103
def registerProductNames(fullClassName: String, binaryClassName: String) =

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

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import tasty.*
99
import config.Printers.{noPrinter, pickling}
1010
import config.Feature
1111
import java.io.PrintStream
12-
import io.FileWriters.TastyWriter
12+
import io.FileWriters.{TastyWriter, ReadOnlyContext}
1313
import StdNames.{str, nme}
1414
import Periods.*
1515
import Phases.*
@@ -22,6 +22,11 @@ import compiletime.uninitialized
2222
import dotty.tools.io.{JarArchive, AbstractFile}
2323
import dotty.tools.dotc.printing.OutlinePrinter
2424
import scala.annotation.constructorOnly
25+
import scala.concurrent.Promise
26+
import dotty.tools.dotc.transform.Pickler.writeSigFilesAsync
27+
28+
import scala.util.chaining.given
29+
import dotty.tools.io.FileWriters.BufferingDelayedReporting
2530

2631
object Pickler {
2732
val name: String = "pickler"
@@ -33,8 +38,62 @@ object Pickler {
3338
*/
3439
inline val ParallelPickling = true
3540

41+
class AsyncTastyHolder(val earlyOut: AbstractFile, val promise: Promise[AsyncTastyState])
42+
class AsyncTastyState(val hasErrors: Boolean, val pending: Option[BufferingDelayedReporting])
43+
44+
// Why we only write to early output in the first run?
45+
// ===================================================
46+
// TL;DR the point of pipeline compilation is to start downstream projects early,
47+
// so we don't want to wait for suspended units to be compiled.
48+
//
49+
// But why is it safe to ignore suspended units?
50+
// If this project contains a transparent macro that is called in the same project,
51+
// the compilation unit of that call will be suspended (if the macro implementation
52+
// is also in this project), causing a second run.
53+
// However before we do that run, we will have already requested sbt to begin
54+
// early downstream compilation. This means that the suspended definitions will not
55+
// be visible in *early* downstream compilation.
56+
//
57+
// However, sbt will by default prevent downstream compilation happening in this scenario,
58+
// due to the existence of macro definitions. So we are protected from failure if user tries
59+
// to use the suspended definitions.
60+
//
61+
// Additionally, it is recommended for the user to move macro implementations to another project
62+
// if they want to force early output. In this scenario the suspensions will no longer occur, so now
63+
// they will become visible in the early-output.
64+
//
65+
// See `sbt-test/pipelining/pipelining-scala-macro` and `sbt-test/pipelining/pipelining-scala-macro-force`
66+
// for examples of this in action.
67+
//
68+
// Therefore we only need to write to early output in the first run. We also provide the option
69+
// to diagnose suspensions with the `-Yno-suspended-units` flag.
70+
def writeSigFilesAsync(
71+
tasks: List[(String, Array[Byte])],
72+
writer: EarlyFileWriter,
73+
promise: Promise[AsyncTastyState])(using ctx: ReadOnlyContext): Unit = {
74+
try
75+
for (internalName, pickled) <- tasks do
76+
val _ = writer.writeTasty(internalName, pickled)
77+
finally
78+
try
79+
writer.close()
80+
finally
81+
promise.success(
82+
AsyncTastyState(
83+
hasErrors = ctx.reporter.hasErrors,
84+
pending = (
85+
ctx.reporter match
86+
case buffered: BufferingDelayedReporting => Some(buffered)
87+
case _ => None
88+
)
89+
)
90+
)
91+
end try
92+
end try
93+
}
94+
3695
class EarlyFileWriter private (writer: TastyWriter, origin: AbstractFile):
37-
def this(dest: AbstractFile)(using @constructorOnly ctx: Context) = this(TastyWriter(dest), dest)
96+
def this(dest: AbstractFile)(using @constructorOnly ctx: ReadOnlyContext) = this(TastyWriter(dest), dest)
3897

3998
export writer.writeTasty
4099

@@ -50,13 +109,15 @@ object Pickler {
50109
class Pickler extends Phase {
51110
import ast.tpd.*
52111

112+
def doAsyncTasty(using Context): Boolean = ctx.asyncTastyPromise.isDefined
113+
53114
override def phaseName: String = Pickler.name
54115

55116
override def description: String = Pickler.description
56117

57118
// No need to repickle trees coming from TASTY
58119
override def isRunnable(using Context): Boolean =
59-
super.isRunnable && !ctx.settings.fromTasty.value
120+
super.isRunnable && (!ctx.settings.fromTasty.value || doAsyncTasty)
60121

61122
// when `-Yjava-tasty` is set we actually want to run this phase on Java sources
62123
override def skipIfJava(using Context): Boolean = false
@@ -86,11 +147,20 @@ class Pickler extends Phase {
86147
*/
87148
object serialized:
88149
val scratch = new ScratchData
150+
private val buf = mutable.ListBuffer.empty[(String, Array[Byte])]
89151
def run(body: ScratchData => Array[Byte]): Array[Byte] =
90152
synchronized {
91153
scratch.reset()
92154
body(scratch)
93155
}
156+
def commit(internalName: String, tasty: Array[Byte]): Unit = synchronized {
157+
buf += ((internalName, tasty))
158+
}
159+
def result(): List[(String, Array[Byte])] = synchronized {
160+
val res = buf.toList
161+
buf.clear()
162+
res
163+
}
94164

95165
private val executor = Executor[Array[Byte]]()
96166

@@ -100,10 +170,29 @@ class Pickler extends Phase {
100170
if isOutline then ctx.fresh.setPrinterFn(OutlinePrinter(_))
101171
else ctx
102172

173+
/** only ran under -Ypickle-write and -from-tasty */
174+
private def runFromTasty(unit: CompilationUnit)(using Context): Unit = {
175+
val pickled = unit.pickled
176+
for (cls, bytes) <- pickled do
177+
serialized.commit(computeInternalName(cls), bytes())
178+
}
179+
180+
private def computeInternalName(cls: ClassSymbol)(using Context): String =
181+
if cls.is(Module) then cls.binaryClassName.stripSuffix(str.MODULE_SUFFIX).nn
182+
else cls.binaryClassName
183+
103184
override def run(using Context): Unit = {
104185
val unit = ctx.compilationUnit
105186
pickling.println(i"unpickling in run ${ctx.runId}")
106187

188+
if ctx.settings.fromTasty.value then
189+
// skip the rest of the phase, as tasty is already "pickled",
190+
// however we still need to set up tasks to write TASTy to
191+
// early output when pipelining is enabled.
192+
if doAsyncTasty then
193+
runFromTasty(unit)
194+
return ()
195+
107196
for
108197
cls <- dropCompanionModuleClasses(topLevelClasses(unit.tpdTree))
109198
tree <- sliceTopLevel(unit.tpdTree, cls)
@@ -137,6 +226,8 @@ class Pickler extends Phase {
137226
val positionWarnings = new mutable.ListBuffer[Message]()
138227
def reportPositionWarnings() = positionWarnings.foreach(report.warning(_))
139228

229+
val internalName = if doAsyncTasty then computeInternalName(cls) else ""
230+
140231
def computePickled(): Array[Byte] = inContext(ctx.fresh) {
141232
serialized.run { scratch =>
142233
treePkl.compactify(scratch)
@@ -166,6 +257,10 @@ class Pickler extends Phase {
166257
println(i"**** pickled info of $cls")
167258
println(TastyPrinter.showContents(pickled, ctx.settings.color.value == "never"))
168259
println(i"**** end of pickled info of $cls")
260+
261+
if doAsyncTasty then
262+
serialized.commit(internalName, pickled)
263+
169264
pickled
170265
}
171266
}
@@ -194,13 +289,27 @@ class Pickler extends Phase {
194289
}
195290

196291
override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = {
292+
val isConcurrent = useExecutor
293+
294+
val writeTask: Option[() => Unit] = ctx.asyncTastyPromise.map: holder =>
295+
() =>
296+
given ReadOnlyContext = if isConcurrent then ReadOnlyContext.buffered else ReadOnlyContext.eager
297+
val writer = Pickler.EarlyFileWriter(holder.earlyOut)
298+
writeSigFilesAsync(serialized.result(), writer, holder.promise)
299+
300+
def runPhase(writeCB: (doWrite: () => Unit) => Unit) =
301+
super.runOn(units).tap(_ => writeTask.foreach(writeCB))
302+
197303
val result =
198-
if useExecutor then
304+
if isConcurrent then
199305
executor.start()
200-
try super.runOn(units)
306+
try
307+
runPhase: doWrite =>
308+
// unless we redesign executor to have "Unit" schedule overload, we need some sentinel value.
309+
executor.schedule(() => { doWrite(); Array.emptyByteArray })
201310
finally executor.close()
202311
else
203-
super.runOn(units)
312+
runPhase(_())
204313
if ctx.settings.YtestPickler.value then
205314
val ctx2 = ctx.fresh
206315
.setSetting(ctx.settings.YreadComments, true)

0 commit comments

Comments
 (0)