Skip to content

Commit e557529

Browse files
authored
Merge pull request #4901 from gzm0/simplify-tracker
Analyzer simplifications after making it parallel
2 parents 3e554a2 + 01f2e96 commit e557529

File tree

1 file changed

+122
-135
lines changed
  • linker/shared/src/main/scala/org/scalajs/linker/analyzer

1 file changed

+122
-135
lines changed

linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala

Lines changed: 122 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ import Infos.{NamespacedMethodName, ReachabilityInfo, ReachabilityInfoInClass}
4545
final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
4646
checkIR: Boolean, failOnError: Boolean, irLoader: IRLoader) {
4747

48-
import Analyzer._
49-
50-
private val allowAddingSyntheticMethods = initial
51-
private val checkAbstractReachability = initial
52-
5348
private val infoLoader: InfoLoader = {
5449
new InfoLoader(irLoader,
5550
if (!checkIR) InfoLoader.NoIRCheck
@@ -58,51 +53,87 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
5853
)
5954
}
6055

61-
private val isNoModule = config.coreSpec.moduleKind == ModuleKind.NoModule
56+
def computeReachability(moduleInitializers: Seq[ModuleInitializer],
57+
symbolRequirements: SymbolRequirement, logger: Logger)(implicit ec: ExecutionContext): Future[Analysis] = {
6258

63-
private var objectClassInfo: ClassInfo = _
64-
private[this] var classLoader: ClassLoader = _
59+
infoLoader.update(logger)
6560

66-
private[this] val _errors = new GrowingList[Error]
61+
val run = new AnalyzerRun(config, initial, infoLoader)(
62+
adjustExecutionContextForParallelism(ec, config.parallel))
6763

68-
private var workTracker: WorkTracker = _
64+
run
65+
.computeReachability(moduleInitializers, symbolRequirements)
66+
.map { _ =>
67+
if (failOnError && run.errors.nonEmpty)
68+
reportErrors(run.errors, logger)
6969

70-
private val fromAnalyzer = FromCore("analyzer")
70+
run
71+
}
72+
.andThen { case _ => infoLoader.cleanAfterRun() }
73+
}
7174

72-
private[this] val _topLevelExportInfos: mutable.Map[(ModuleID, String), TopLevelExportInfo] = emptyThreadSafeMap
75+
private def reportErrors(errors: List[Error], logger: Logger): Unit = {
76+
require(errors.nonEmpty)
7377

74-
def computeReachability(moduleInitializers: Seq[ModuleInitializer],
75-
symbolRequirements: SymbolRequirement, logger: Logger)(implicit ec: ExecutionContext): Future[Analysis] = {
78+
val maxDisplayErrors = {
79+
val propName = "org.scalajs.linker.maxlinkingerrors"
80+
Try(System.getProperty(propName, "20").toInt).getOrElse(20).max(1)
81+
}
7682

77-
computeInternal(moduleInitializers, symbolRequirements, logger)(
78-
adjustExecutionContextForParallelism(ec, config.parallel))
83+
errors
84+
.take(maxDisplayErrors)
85+
.foreach(logError(_, logger, Level.Error))
86+
87+
val skipped = errors.size - maxDisplayErrors
88+
if (skipped > 0)
89+
logger.log(Level.Error, s"Not showing $skipped more linking errors")
90+
91+
if (initial) {
92+
throw new LinkingException("There were linking errors")
93+
} else {
94+
throw new AssertionError(
95+
"There were linking errors after the optimizer has run. " +
96+
"This is a bug, please report it. " +
97+
"You can work around the bug by disabling the optimizer. " +
98+
"In the sbt plugin, this can be done with " +
99+
"`scalaJSLinkerConfig ~= { _.withOptimizer(false) }`.")
100+
}
79101
}
102+
}
80103

81-
/** Internal helper to isolate the execution context. */
82-
private def computeInternal(moduleInitializers: Seq[ModuleInitializer],
83-
symbolRequirements: SymbolRequirement, logger: Logger)(implicit ec: ExecutionContext): Future[Analysis] = {
104+
private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
105+
infoLoader: InfoLoader)(implicit ec: ExecutionContext) extends Analysis {
106+
import AnalyzerRun._
84107

85-
resetState()
108+
private val allowAddingSyntheticMethods = initial
109+
private val checkAbstractReachability = initial
86110

87-
infoLoader.update(logger)
111+
private val isNoModule = config.coreSpec.moduleKind == ModuleKind.NoModule
112+
113+
private val workTracker: WorkTracker = new WorkTracker
114+
private[this] val classLoader: ClassLoader = new ClassLoader
115+
116+
private var objectClassInfo: ClassInfo = _
117+
private var _classInfos: scala.collection.Map[ClassName, ClassInfo] = _
118+
119+
def classInfos: scala.collection.Map[ClassName, Analysis.ClassInfo] = _classInfos
88120

89-
workTracker = new WorkTracker
90-
classLoader = new ClassLoader
121+
private[this] val _errors = new GrowingList[Error]
122+
123+
override def errors: List[Error] = _errors.get()
124+
125+
private val fromAnalyzer = FromCore("analyzer")
91126

127+
private[this] val _topLevelExportInfos: mutable.Map[(ModuleID, String), TopLevelExportInfo] = emptyThreadSafeMap
128+
def topLevelExportInfos: scala.collection.Map[(ModuleID, String), Analysis.TopLevelExportInfo] = _topLevelExportInfos
129+
130+
def computeReachability(moduleInitializers: Seq[ModuleInitializer],
131+
symbolRequirements: SymbolRequirement): Future[Unit] = {
92132
loadObjectClass(() => loadEverything(moduleInitializers, symbolRequirements))
93133

94134
workTracker
95-
.future
96-
.map(_ => postLoad(moduleInitializers, logger))
97-
.andThen { case _ => infoLoader.cleanAfterRun() }
98-
}
99-
100-
private def resetState(): Unit = {
101-
objectClassInfo = null
102-
workTracker = null
103-
_errors.clear()
104-
classLoader = null
105-
_topLevelExportInfos.clear()
135+
.allowComplete()
136+
.map(_ => postLoad(moduleInitializers))
106137
}
107138

108139
private def loadObjectClass(onSuccess: () => Unit): Unit = {
@@ -154,8 +185,9 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
154185
reachInitializers(moduleInitializers)
155186
}
156187

157-
private def postLoad(moduleInitializers: Seq[ModuleInitializer],
158-
logger: Logger): Analysis = {
188+
private def postLoad(moduleInitializers: Seq[ModuleInitializer]): Unit = {
189+
_classInfos = classLoader.loadedInfos()
190+
159191
if (isNoModule) {
160192
// Check there is only a single module.
161193
val publicModuleIDs = (
@@ -167,51 +199,8 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
167199
_errors ::= MultiplePublicModulesWithoutModuleSupport(publicModuleIDs)
168200
}
169201

170-
val infos = classLoader.loadedInfos()
171-
172202
// Reach additional data, based on reflection methods used
173-
reachDataThroughReflection(infos)
174-
175-
val errs = _errors.get()
176-
177-
if (failOnError && errs.nonEmpty)
178-
reportErrors(logger)
179-
180-
new Analysis {
181-
val classInfos = infos
182-
val topLevelExportInfos = _topLevelExportInfos
183-
val errors = errs
184-
}
185-
}
186-
187-
private def reportErrors(logger: Logger): Unit = {
188-
val errors = _errors.get()
189-
190-
require(errors.nonEmpty)
191-
192-
val maxDisplayErrors = {
193-
val propName = "org.scalajs.linker.maxlinkingerrors"
194-
Try(System.getProperty(propName, "20").toInt).getOrElse(20).max(1)
195-
}
196-
197-
errors
198-
.take(maxDisplayErrors)
199-
.foreach(logError(_, logger, Level.Error))
200-
201-
val skipped = errors.size - maxDisplayErrors
202-
if (skipped > 0)
203-
logger.log(Level.Error, s"Not showing $skipped more linking errors")
204-
205-
if (initial) {
206-
throw new LinkingException("There were linking errors")
207-
} else {
208-
throw new AssertionError(
209-
"There were linking errors after the optimizer has run. " +
210-
"This is a bug, please report it. " +
211-
"You can work around the bug by disabling the optimizer. " +
212-
"In the sbt plugin, this can be done with " +
213-
"`scalaJSLinkerConfig ~= { _.withOptimizer(false) }`.")
214-
}
203+
reachDataThroughReflection()
215204
}
216205

217206
private def reachSymbolRequirement(requirement: SymbolRequirement): Unit = {
@@ -304,10 +293,9 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
304293
}
305294

306295
/** Reach additional class data based on reflection methods being used. */
307-
private def reachDataThroughReflection(
308-
classInfos: scala.collection.Map[ClassName, ClassInfo]): Unit = {
296+
private def reachDataThroughReflection(): Unit = {
309297

310-
val classClassInfo = classInfos.get(ClassClass)
298+
val classClassInfo = _classInfos.get(ClassClass)
311299

312300
/* If Class.getSuperclass() is reachable, we can reach the data of all
313301
* superclasses of classes whose data we can already reach.
@@ -320,7 +308,7 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
320308
// calledFrom should always be nonEmpty if isReachable, but let's be robust
321309
implicit val from =
322310
getSuperclassMethodInfo.calledFrom.headOption.getOrElse(fromAnalyzer)
323-
for (classInfo <- classInfos.values.filter(_.isDataAccessed).toList) {
311+
for (classInfo <- _classInfos.values.filter(_.isDataAccessed).toList) {
324312
@tailrec
325313
def loop(classInfo: ClassInfo): Unit = {
326314
classInfo.accessData()
@@ -339,14 +327,16 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
339327

340328
private def lookupClass(className: ClassName)(
341329
onSuccess: ClassInfo => Unit)(implicit from: From): Unit = {
342-
workTracker.track(classLoader.lookupClass(className)) {
343-
case info: ClassInfo =>
344-
info.link()
345-
onSuccess(info)
346-
347-
case CycleInfo(cycle, root) =>
348-
assert(root == null, s"unresolved root: $root")
349-
_errors ::= CycleInInheritanceChain(cycle, fromAnalyzer)
330+
workTracker.track {
331+
classLoader.lookupClass(className).map {
332+
case info: ClassInfo =>
333+
info.link()
334+
onSuccess(info)
335+
336+
case CycleInfo(cycle, root) =>
337+
assert(root == null, s"unresolved root: $root")
338+
_errors ::= CycleInInheritanceChain(cycle, fromAnalyzer)
339+
}
350340
}
351341
}
352342

@@ -831,19 +821,18 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
831821
})
832822
}
833823

834-
def tryLookupReflProxyMethod(proxyName: MethodName)(
835-
onSuccess: MethodInfo => Unit)(implicit from: From): Unit = {
824+
private def maybeReachReflProxyMethod(proxyName: MethodName)(implicit from: From): Unit = {
836825
if (!allowAddingSyntheticMethods) {
837-
tryLookupMethod(proxyName).foreach(onSuccess)
826+
tryLookupMethod(proxyName).foreach(_.reach(this))
838827
} else {
839-
publicMethodInfos.get(proxyName).fold {
840-
findReflectiveTarget(proxyName)(onSuccess)
841-
} (onSuccess)
828+
publicMethodInfos
829+
.get(proxyName)
830+
.fold(findAndReachReflectiveTarget(proxyName))(_.reach(this))
842831
}
843832
}
844833

845-
private def findReflectiveTarget(proxyName: MethodName)(
846-
onSuccess: MethodInfo => Unit)(implicit from: From): Unit = {
834+
private def findAndReachReflectiveTarget(
835+
proxyName: MethodName)(implicit from: From): Unit = {
847836
/* The lookup for a target method in this code implements the
848837
* algorithm defining `java.lang.Class.getMethod`. This mimics how
849838
* reflective calls are implemented on the JVM, at link time.
@@ -881,15 +870,16 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
881870

882871
case onlyCandidate :: Nil =>
883872
// Fast path that does not require workTracker.track
884-
val proxy = createReflProxy(proxyName, onlyCandidate.methodName)
885-
onSuccess(proxy)
873+
createReflProxy(proxyName, onlyCandidate.methodName).reach(this)
886874

887875
case _ =>
888-
val targetFuture = computeMostSpecificProxyMatch(candidates)
889-
workTracker.track(targetFuture) { reflectiveTarget =>
890-
val proxy = createReflProxy(proxyName, reflectiveTarget.methodName)
891-
onSuccess(proxy)
876+
val future = for {
877+
reflectiveTarget <- computeMostSpecificProxyMatch(candidates)
878+
} yield {
879+
createReflProxy(proxyName, reflectiveTarget.methodName).reach(this)
892880
}
881+
882+
workTracker.track(future)
893883
}
894884
}
895885

@@ -933,8 +923,6 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
933923
private def computeMostSpecificProxyMatch(candidates: List[MethodInfo])(
934924
implicit from: From): Future[MethodInfo] = {
935925

936-
implicit val ec = workTracker.ec
937-
938926
/* From the JavaDoc of java.lang.Class.getMethod:
939927
*
940928
* If more than one [candidate] method is found in C, and one of these
@@ -1215,7 +1203,7 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
12151203
private def callMethodResolved(methodName: MethodName)(
12161204
implicit from: From): Unit = {
12171205
if (methodName.isReflectiveProxy) {
1218-
tryLookupReflProxyMethod(methodName)(_.reach(this))
1206+
maybeReachReflProxyMethod(methodName)
12191207
} else {
12201208
lookupMethod(methodName).reach(this)
12211209
}
@@ -1578,46 +1566,45 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
15781566

15791567
}
15801568

1581-
object Analyzer {
1569+
private object AnalyzerRun {
15821570
private val getSuperclassMethodName =
15831571
MethodName("getSuperclass", Nil, ClassRef(ClassClass))
15841572

1585-
private class WorkTracker(implicit val ec: ExecutionContext) {
1573+
private class WorkTracker(implicit ec: ExecutionContext) {
15861574
private val pending = new AtomicInteger(0)
1575+
@volatile private var _allowComplete = false
15871576
private val promise = Promise[Unit]()
15881577

1589-
def track[T](fut: Future[T])(onSuccess: T => Unit): Unit = {
1590-
val got = pending.incrementAndGet()
1591-
assert(got > 0)
1578+
def track(fut: Future[Unit]): Unit = {
1579+
pending.incrementAndGet()
1580+
1581+
fut.onComplete {
1582+
case Success(_) =>
1583+
if (pending.decrementAndGet() == 0)
1584+
tryComplete()
15921585

1593-
fut.map(onSuccess).onComplete {
1594-
case Success(_) => taskDone()
1595-
case Failure(t) => promise.tryFailure(t)
1586+
case Failure(t) =>
1587+
promise.tryFailure(t)
15961588
}
15971589
}
15981590

1599-
private def taskDone(): Unit = {
1600-
if (pending.decrementAndGet() == 0) {
1601-
/* TODO: The completion condition in the WorkTracker is not what we want:
1602-
*
1603-
* What we have is: The number of pending tasks drops to 0.
1604-
*
1605-
* What we want is: The number of pending tasks drops to 0 after a
1606-
* certain point in the main execution flow has been reached.
1607-
*
1608-
* This is currently not a problem, because `loadObjectClass` submits the
1609-
* initial task and then everything else is done inside a task
1610-
* (until `postLoad`).
1611-
*
1612-
* However, this is not strictly necessary, we could, for example, start
1613-
* loading infos for other entrypoints in parallel. So we should fix this.
1614-
*/
1615-
pending.set(-1)
1591+
private def tryComplete(): Unit = {
1592+
/* Note that after _allowComplete is true and pending == 0, we are sure
1593+
* that no new task will be submitted concurrently:
1594+
* - _allowComplete guarantees us that no external task will be added anymore
1595+
* - pending == 0 guarantees us that no internal task (which might create
1596+
* more tasks) are running anymore.
1597+
*/
1598+
if (_allowComplete && pending.get() == 0) {
16161599
promise.trySuccess(())
16171600
}
16181601
}
16191602

1620-
def future: Future[Unit] = promise.future
1603+
def allowComplete(): Future[Unit] = {
1604+
_allowComplete = true
1605+
tryComplete()
1606+
promise.future
1607+
}
16211608
}
16221609

16231610
private final class GrowingList[A] {

0 commit comments

Comments
 (0)