Skip to content

Commit

Permalink
RFC: Consolidate Info Loading
Browse files Browse the repository at this point in the history
  • Loading branch information
gzm0 committed Mar 5, 2023
1 parent c843819 commit 47f8b0f
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 375 deletions.
Expand Up @@ -42,7 +42,7 @@ private final class Analyzer(config: CommonPhaseConfig,
symbolRequirements: SymbolRequirement,
allowAddingSyntheticMethods: Boolean,
checkAbstractReachability: Boolean,
inputProvider: Analyzer.InputProvider,
infoLoader: InfoLoader,
ec: ExecutionContext)
extends Analysis {

Expand Down Expand Up @@ -85,7 +85,7 @@ private final class Analyzer(config: CommonPhaseConfig,
/* Load the java.lang.Object class, and validate it
* If it is missing or invalid, we're in deep trouble, and cannot continue.
*/
inputProvider.loadInfo(ObjectClass)(ec) match {
infoLoader.loadInfo(ObjectClass)(ec) match {
case None =>
_errors += MissingJavaLangObjectClass(fromAnalyzer)

Expand Down Expand Up @@ -128,7 +128,7 @@ private final class Analyzer(config: CommonPhaseConfig,
reachSymbolRequirement(symbolRequirements)

// Reach entry points
for (className <- inputProvider.classesWithEntryPoints())
for (className <- infoLoader.classesWithEntryPoints())
lookupClass(className)(_.reachEntryPoints())

// Reach module initializers.
Expand Down Expand Up @@ -350,7 +350,7 @@ private final class Analyzer(config: CommonPhaseConfig,

_classInfos(className) = this

inputProvider.loadInfo(className)(ec) match {
infoLoader.loadInfo(className)(ec) match {
case Some(future) =>
workQueue.enqueue(future)(link(_, nonExistent = false))

Expand Down Expand Up @@ -1481,19 +1481,12 @@ object Analyzer {
symbolRequirements: SymbolRequirement,
allowAddingSyntheticMethods: Boolean,
checkAbstractReachability: Boolean,
inputProvider: InputProvider)(implicit ec: ExecutionContext): Future[Analysis] = {
infoLoader: InfoLoader)(implicit ec: ExecutionContext): Future[Analysis] = {
val analyzer = new Analyzer(config, moduleInitializers, symbolRequirements,
allowAddingSyntheticMethods, checkAbstractReachability, inputProvider, ec)
allowAddingSyntheticMethods, checkAbstractReachability, infoLoader, ec)
analyzer.computeReachability().map(_ => analyzer)
}

trait InputProvider {
def classesWithEntryPoints(): Iterable[ClassName]

def loadInfo(className: ClassName)(
implicit ec: ExecutionContext): Option[Future[Infos.ClassInfo]]
}

private class WorkQueue(ec: ExecutionContext) {
private val queue = new ConcurrentLinkedQueue[() => Unit]()
private val working = new AtomicBoolean(false)
Expand Down
@@ -0,0 +1,259 @@
/*
* Scala.js (https://www.scala-js.org/)
*
* Copyright EPFL.
*
* Licensed under Apache License 2.0
* (https://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package org.scalajs.linker.analyzer

import scala.concurrent._

import scala.collection.mutable

import org.scalajs.ir.{EntryPointsInfo, Version}
import org.scalajs.ir.Names._
import org.scalajs.ir.Trees._

import org.scalajs.logging._

import org.scalajs.linker.checker.ClassDefChecker
import org.scalajs.linker.frontend.IRLoader
import org.scalajs.linker.interface.LinkingException
import org.scalajs.linker.CollectionsCompat.MutableMapCompatOps

final class InfoLoader(irLoader: IRLoader, checkIR: Boolean, allowReflectiveProxies: Boolean, allowTransients: Boolean) {
private var logger: Logger = _
private val cache = mutable.Map.empty[ClassName, ClassInfoCache]

def update(logger: Logger): Unit = {
this.logger = logger
}

def classesWithEntryPoints(): Iterable[ClassName] =
irLoader.classesWithEntryPoints()

def loadInfo(className: ClassName)(
implicit ec: ExecutionContext): Option[Future[Infos.ClassInfo]] = {
if (irLoader.classExists(className)) {
val infoCache = cache.getOrElseUpdate(className, new ClassInfoCache(className, irLoader, checkIR, allowReflectiveProxies, allowTransients))
Some(infoCache.loadInfo(logger))
} else {
None
}
}

def cleanAfterRun(): Unit = {
logger = null
cache.filterInPlace((_, infoCache) => infoCache.cleanAfterRun())
}
}

private class ClassInfoCache(className: ClassName, irLoader: IRLoader, checkIR: Boolean, allowReflectiveProxies: Boolean, allowTransients: Boolean) {
private var cacheUsed: Boolean = false
private var version: Version = Version.Unversioned
private var info: Future[Infos.ClassInfo] = _

private val methodsInfoCaches = MethodDefsInfosCache()
private val jsConstructorInfoCache = new JSConstructorDefInfoCache()
private val exportedMembersInfoCaches = JSMethodPropDefsInfosCache()

def loadInfo(logger: Logger)(implicit ec: ExecutionContext): Future[Infos.ClassInfo] = synchronized {
if (!cacheUsed) {
cacheUsed = true

val newVersion = irLoader.classVersion(className)
if (!version.sameVersion(newVersion)) {
version = newVersion
info = irLoader.loadClassDef(className).map { tree =>
if (checkIR) {
val errorCount = ClassDefChecker.check(
tree, allowReflectiveProxies, allowTransients, logger)
if (errorCount != 0) {
throw new LinkingException(
s"There were $errorCount ClassDef checking errors.")
}
}

generateInfos(tree)
}
}
}

info
}

private def generateInfos(classDef: ClassDef): Infos.ClassInfo = {
val builder = new Infos.ClassInfoBuilder(classDef.className,
classDef.kind, classDef.superClass.map(_.name),
classDef.interfaces.map(_.name), classDef.jsNativeLoadSpec)

classDef.fields.foreach {
case FieldDef(flags, FieldIdent(name), _, ftpe) =>
if (!flags.namespace.isStatic)
builder.maybeAddReferencedFieldClass(name, ftpe)

case _: JSFieldDef =>
// Nothing to do.
}

classDef.methods.foreach { method =>
builder.addMethod(methodsInfoCaches.getInfo(method))
}

classDef.jsConstructor.foreach { jsConstructor =>
builder.addExportedMember(jsConstructorInfoCache.getInfo(jsConstructor))
}

for (info <- exportedMembersInfoCaches.getInfos(classDef.jsMethodProps))
builder.addExportedMember(info)

/* We do not cache top-level exports, because they're quite rare,
* and usually quite small when they exist.
*/
classDef.topLevelExportDefs.foreach { topLevelExportDef =>
builder.addTopLevelExport(Infos.generateTopLevelExportInfo(classDef.name.name, topLevelExportDef))
}

classDef.jsNativeMembers.foreach(builder.addJSNativeMember(_))

builder.result()
}

/** Returns true if the cache has been used and should be kept. */
def cleanAfterRun(): Boolean = synchronized {
val result = cacheUsed
cacheUsed = false
if (result) {
// No point in cleaning the inner caches if the whole class disappears
methodsInfoCaches.cleanAfterRun()
jsConstructorInfoCache.cleanAfterRun()
exportedMembersInfoCaches.cleanAfterRun()
}
result
}
}

private final class MethodDefsInfosCache private (
val caches: Array[mutable.Map[MethodName, MethodDefInfoCache]])
extends AnyVal {

def getInfo(methodDef: MethodDef): Infos.MethodInfo = {
val cache = caches(methodDef.flags.namespace.ordinal)
.getOrElseUpdate(methodDef.methodName, new MethodDefInfoCache)
cache.getInfo(methodDef)
}

def cleanAfterRun(): Unit = {
caches.foreach(_.filterInPlace((_, cache) => cache.cleanAfterRun()))
}
}

private object MethodDefsInfosCache {
def apply(): MethodDefsInfosCache = {
new MethodDefsInfosCache(
Array.fill(MemberNamespace.Count)(mutable.Map.empty))
}
}

/* For JS method and property definitions, we use their index in the list of
* `linkedClass.exportedMembers` as their identity. We cannot use their name
* because the name itself is a `Tree`.
*
* If there is a different number of exported members than in a previous run,
* we always recompute everything. This is fine because, for any given class,
* either all JS methods and properties are reachable, or none are. So we're
* only missing opportunities for incrementality in the case where JS members
* are added or removed in the original .sjsir, which is not a big deal.
*/
private final class JSMethodPropDefsInfosCache private (
private var caches: Array[JSMethodPropDefInfoCache]) {

def getInfos(members: List[JSMethodPropDef]): List[Infos.ReachabilityInfo] = {
if (members.isEmpty) {
caches = null
Nil
} else {
val membersSize = members.size
if (caches == null || membersSize != caches.size)
caches = Array.fill(membersSize)(new JSMethodPropDefInfoCache)

for ((member, i) <- members.zipWithIndex) yield {
caches(i).getInfo(member)
}
}
}

def cleanAfterRun(): Unit = {
if (caches != null)
caches.foreach(_.cleanAfterRun())
}
}

private object JSMethodPropDefsInfosCache {
def apply(): JSMethodPropDefsInfosCache =
new JSMethodPropDefsInfosCache(null)
}

private abstract class AbstractMemberInfoCache[Def <: VersionedMemberDef, Info] {
private var cacheUsed: Boolean = false
private var lastVersion: Version = Version.Unversioned
private var info: Info = _

final def getInfo(member: Def): Info = {
update(member)
info
}

private final def update(member: Def): Unit = {
if (!cacheUsed) {
cacheUsed = true
val newVersion = member.version
if (!lastVersion.sameVersion(newVersion)) {
info = computeInfo(member)
lastVersion = newVersion
}
}
}

protected def computeInfo(member: Def): Info

/** Returns true if the cache has been used and should be kept. */
final def cleanAfterRun(): Boolean = {
val result = cacheUsed
cacheUsed = false
result
}
}

private final class MethodDefInfoCache
extends AbstractMemberInfoCache[MethodDef, Infos.MethodInfo] {

protected def computeInfo(member: MethodDef): Infos.MethodInfo =
Infos.generateMethodInfo(member)
}

private final class JSConstructorDefInfoCache
extends AbstractMemberInfoCache[JSConstructorDef, Infos.ReachabilityInfo] {

protected def computeInfo(member: JSConstructorDef): Infos.ReachabilityInfo =
Infos.generateJSConstructorInfo(member)
}

private final class JSMethodPropDefInfoCache
extends AbstractMemberInfoCache[JSMethodPropDef, Infos.ReachabilityInfo] {

protected def computeInfo(member: JSMethodPropDef): Infos.ReachabilityInfo = {
member match {
case methodDef: JSMethodDef =>
Infos.generateJSMethodInfo(methodDef)
case propertyDef: JSPropertyDef =>
Infos.generateJSPropertyInfo(propertyDef)
}
}
}
Expand Up @@ -468,49 +468,6 @@ object Infos {
else set.toList
}

/** Generates the [[ClassInfo]] of a
* [[org.scalajs.ir.Trees.ClassDef Trees.ClassDef]].
*/
def generateClassInfo(classDef: ClassDef): ClassInfo = {
val builder = new ClassInfoBuilder(classDef.name.name, classDef.kind,
classDef.superClass.map(_.name), classDef.interfaces.map(_.name),
classDef.jsNativeLoadSpec)

classDef.fields foreach {
case FieldDef(flags, FieldIdent(name), _, ftpe) =>
if (!flags.namespace.isStatic) {
builder.maybeAddReferencedFieldClass(name, ftpe)
}

case _: JSFieldDef =>
// Nothing to do.
}

classDef.methods.foreach { methodDef =>
builder.addMethod(generateMethodInfo(methodDef))
}

classDef.jsConstructor.foreach { ctorDef =>
builder.addExportedMember(generateJSConstructorInfo(ctorDef))
}

classDef.jsMethodProps.foreach {
case methodDef: JSMethodDef =>
builder.addExportedMember(generateJSMethodInfo(methodDef))

case propertyDef: JSPropertyDef =>
builder.addExportedMember(generateJSPropertyInfo(propertyDef))
}

classDef.jsNativeMembers.foreach(builder.addJSNativeMember(_))

classDef.topLevelExportDefs.foreach { topLevelExportDef =>
builder.addTopLevelExport(generateTopLevelExportInfo(classDef.name.name, topLevelExportDef))
}

builder.result()
}

/** Generates the [[MethodInfo]] of a
* [[org.scalajs.ir.Trees.MethodDef Trees.MethodDef]].
*/
Expand Down

0 comments on commit 47f8b0f

Please sign in to comment.