Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consolidate Info Loading #4824

Merged
merged 2 commits into from Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,284 @@
/*
* 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, irCheckMode: InfoLoader.IRCheckMode) {
private var logger: Logger = _
private val cache = mutable.Map.empty[ClassName, InfoLoader.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 InfoLoader.ClassInfoCache(className, irLoader, irCheckMode))
Some(infoCache.loadInfo(logger))
} else {
None
}
}

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

object InfoLoader {
sealed trait IRCheckMode

case object NoIRCheck extends IRCheckMode
case object InitialIRCheck extends IRCheckMode
case object InternalIRCheck extends IRCheckMode

private class ClassInfoCache(className: ClassName, irLoader: IRLoader, irCheckMode: InfoLoader.IRCheckMode) {
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 the cache was already used in this run, the classDef and info are
* already correct, no matter what the versions say.
*/
if (!cacheUsed) {
cacheUsed = true

val newVersion = irLoader.irFileVersion(className)
if (!version.sameVersion(newVersion)) {
version = newVersion
info = irLoader.loadClassDef(className).map { tree =>
irCheckMode match {
case InfoLoader.NoIRCheck =>
// no check

case InfoLoader.InitialIRCheck =>
val errorCount = ClassDefChecker.check(tree,
allowReflectiveProxies = false, allowTransients = false, logger)
if (errorCount != 0) {
throw new LinkingException(
s"There were $errorCount ClassDef checking errors.")
}

case InfoLoader.InternalIRCheck =>
val errorCount = ClassDefChecker.check(tree,
allowReflectiveProxies = true, allowTransients = true, logger)
if (errorCount != 0) {
throw new LinkingException(
s"There were $errorCount ClassDef checking errors after optimizing. " +
"Please report this as a bug.")
}
}

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