diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala index 622daf283f..795a1cc33c 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala @@ -28,6 +28,7 @@ import org.scalajs.linker.standard.ModuleSet.ModuleID import org.scalajs.linker.backend.emitter.Emitter import org.scalajs.linker.backend.javascript.{ByteArrayWriter, Printers, SourceMapWriter, Trees => js} +import org.scalajs.linker.caching._ /** The basic backend for the Scala.js linker. * @@ -185,7 +186,8 @@ private object BasicLinkerBackend { private var _footerBytesCache: Array[Byte] = null private var _headerNewLineCountCache: Int = 0 - private val modules = new java.util.concurrent.ConcurrentHashMap[ModuleID, PrintedModuleCache] + private val modules: ConcurrentCacheMap[ModuleID, PrintedModuleCache] = + key => new PrintedModuleCache def updateGlobal(header: String, footer: String): Boolean = { if (header == lastHeader && footer == lastFooter) { @@ -204,33 +206,17 @@ private object BasicLinkerBackend { def footerBytes: Array[Byte] = _footerBytesCache def headerNewLineCount: Int = _headerNewLineCountCache - def getModuleCache(moduleID: ModuleID): PrintedModuleCache = { - val result = modules.computeIfAbsent(moduleID, _ => new PrintedModuleCache) - result.startRun() - result - } + def getModuleCache(moduleID: ModuleID): PrintedModuleCache = + modules.get(moduleID) - def cleanAfterRun(): Unit = { - val iter = modules.entrySet().iterator() - while (iter.hasNext()) { - val moduleCache = iter.next().getValue() - if (!moduleCache.cleanAfterRun()) { - iter.remove() - } - } - } + def cleanAfterRun(): Unit = + modules.cleanAfterRun() } - private sealed class PrintedModuleCache { - private var cacheUsed = false - + private final class PrintedModuleCache extends Cache { private var previousFinalJSFileSize: Int = 0 private var previousFinalSourceMapSize: Int = 0 - def startRun(): Unit = { - cacheUsed = true - } - def getPreviousFinalJSFileSize(): Int = previousFinalJSFileSize def getPreviousFinalSourceMapSize(): Int = previousFinalSourceMapSize @@ -239,11 +225,5 @@ private object BasicLinkerBackend { previousFinalJSFileSize = finalJSFileSize previousFinalSourceMapSize = finalSourceMapSize } - - def cleanAfterRun(): Boolean = { - val wasUsed = cacheUsed - cacheUsed = false - wasUsed - } } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index c6576f4f5d..1297316fc3 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -14,8 +14,6 @@ package org.scalajs.linker.backend.emitter import scala.annotation.tailrec -import scala.collection.mutable - import org.scalajs.ir.{ClassKind, Position, Version} import org.scalajs.ir.Names._ import org.scalajs.ir.OriginalName.NoOriginalName @@ -27,7 +25,7 @@ import org.scalajs.linker.interface._ import org.scalajs.linker.standard._ import org.scalajs.linker.standard.ModuleSet.ModuleID import org.scalajs.linker.backend.javascript.{Trees => js, _} -import org.scalajs.linker.CollectionsCompat.MutableMapCompatOps +import org.scalajs.linker.caching._ import EmitterNames._ import GlobalRefUtils._ @@ -71,9 +69,11 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { val coreJSLibCache: CoreJSLibCache = new CoreJSLibCache - val moduleCaches: mutable.Map[ModuleID, ModuleCache] = mutable.Map.empty + val moduleCaches: CacheMap[ModuleID, ModuleCache] = + (key) => new ModuleCache - val classCaches: mutable.Map[ClassID, ClassCache] = mutable.Map.empty + val classCaches: CacheMap[ClassID, ClassCache] = + (key) => new ClassCache } private var state: State = new State(Set.empty) @@ -81,7 +81,7 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { private def jsGen: JSGen = state.sjsGen.jsGen private def sjsGen: SJSGen = state.sjsGen private def classEmitter: ClassEmitter = state.classEmitter - private def classCaches: mutable.Map[ClassID, ClassCache] = state.classCaches + private def classCaches: CacheMap[ClassID, ClassCache] = state.classCaches private[this] var statsClassesReused: Int = 0 private[this] var statsClassesInvalidated: Int = 0 @@ -150,12 +150,9 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { val invalidateAll = knowledgeGuardian.update(moduleSet) if (invalidateAll) { state.coreJSLibCache.invalidate() - classCaches.clear() + classCaches.invalidate() } - // Inform caches about new run. - classCaches.valuesIterator.foreach(_.startRun()) - try { emitAvoidGlobalClash(moduleSet, logger, secondAttempt = false) } finally { @@ -169,8 +166,8 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { logger.debug(s"Emitter: Pre prints: $statsPrePrints") // Inform caches about run completion. - state.moduleCaches.filterInPlace((_, c) => c.cleanAfterRun()) - classCaches.filterInPlace((_, c) => c.cleanAfterRun()) + state.moduleCaches.cleanAfterRun() + classCaches.cleanAfterRun() } } @@ -241,7 +238,7 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { } val moduleContext = ModuleContext.fromModule(module) - val moduleCache = state.moduleCaches.getOrElseUpdate(module.id, new ModuleCache) + val moduleCache = state.moduleCaches.get(module.id) val moduleClasses = generatedClasses(module.id) @@ -414,8 +411,8 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { moduleContext: ModuleContext): GeneratedClass = { val className = linkedClass.className - val classCache = classCaches.getOrElseUpdate( - new ClassID(linkedClass.ancestors, moduleContext), new ClassCache) + val classCache = + classCaches.get(new ClassID(linkedClass.ancestors, moduleContext)) var changed = false def extractChanged[T](x: (T, Boolean)): T = { @@ -758,8 +755,6 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { // Caching private final class ModuleCache extends knowledgeGuardian.KnowledgeAccessor { - private[this] var _cacheUsed: Boolean = false - private[this] var _importsCache: WithGlobals[List[js.Tree]] = WithGlobals.nil private[this] var _lastExternalDependencies: Set[String] = Set.empty private[this] var _lastInternalDependencies: Set[ModuleID] = Set.empty @@ -790,7 +785,7 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { def getOrComputeImports(externalDependencies: Set[String], internalDependencies: Set[ModuleID])( compute: => WithGlobals[List[js.Tree]]): (WithGlobals[List[js.Tree]], Boolean) = { - _cacheUsed = true + markUsed() if (externalDependencies != _lastExternalDependencies || internalDependencies != _lastInternalDependencies) { _importsCache = compute @@ -806,7 +801,7 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { def getOrComputeTopLevelExports(topLevelExports: List[LinkedTopLevelExport])( compute: => WithGlobals[List[js.Tree]]): (WithGlobals[List[js.Tree]], Boolean) = { - _cacheUsed = true + markUsed() if (!sameTopLevelExports(topLevelExports, _lastTopLevelExports)) { _topLevelExportsCache = compute @@ -847,7 +842,7 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { def getOrComputeInitializers(initializers: List[ModuleInitializer.Initializer])( compute: => WithGlobals[List[js.Tree]]): (WithGlobals[List[js.Tree]], Boolean) = { - _cacheUsed = true + markUsed() if (initializers != _lastInitializers) { _initializersCache = compute @@ -857,145 +852,85 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { (_initializersCache, false) } } - - def cleanAfterRun(): Boolean = { - val result = _cacheUsed - _cacheUsed = false - result - } } - private final class ClassCache extends knowledgeGuardian.KnowledgeAccessor { - private[this] var _cache: DesugaredClassCache = null - private[this] var _lastVersion: Version = Version.Unversioned - private[this] var _cacheUsed = false + private final class ClassCache + extends knowledgeGuardian.KnowledgeAccessor + with VersionedCache[DesugaredClassCache] { - private[this] val _methodCaches = - Array.fill(MemberNamespace.Count)(mutable.Map.empty[MethodName, MethodCache[List[js.Tree]]]) + private[this] val _methodCaches: NamespacedMethodCacheMap[MethodCache[List[js.Tree]]] = + (key) => new MethodCache() - private[this] val _memberMethodCache = - mutable.Map.empty[MethodName, MethodCache[List[js.Tree]]] + private[this] val _memberMethodCache: CacheMap[MethodName, MethodCache[List[js.Tree]]] = + (key) => new MethodCache() - private[this] var _constructorCache: Option[MethodCache[List[js.Tree]]] = None + private[this] val _constructorCache: CacheOption[MethodCache[List[js.Tree]]] = + () => new MethodCache() - private[this] val _exportedMembersCache = - mutable.Map.empty[Int, MethodCache[List[js.Tree]]] + private[this] val _exportedMembersCache: CacheMap[Int, MethodCache[List[js.Tree]]] = + (key) => new MethodCache() - private[this] var _fullClassChangeTracker: Option[FullClassChangeTracker] = None - - override def invalidate(): Unit = { - /* Do not invalidate contained methods, as they have their own - * invalidation logic. - */ - super.invalidate() - _cache = null - _lastVersion = Version.Unversioned - } - - def startRun(): Unit = { - _cacheUsed = false - _methodCaches.foreach(_.valuesIterator.foreach(_.startRun())) - _memberMethodCache.valuesIterator.foreach(_.startRun()) - _constructorCache.foreach(_.startRun()) - _fullClassChangeTracker.foreach(_.startRun()) - } + private[this] val _fullClassChangeTracker: CacheOption[FullClassChangeTracker] = + () => new FullClassChangeTracker() def getCache(version: Version): (DesugaredClassCache, Boolean) = { - _cacheUsed = true - if (_cache == null || !_lastVersion.sameVersion(version)) { - invalidate() + val result = this.getOrComputeWithChanged(version, new DesugaredClassCache) + if (result._2) statsClassesInvalidated += 1 - _lastVersion = version - _cache = new DesugaredClassCache - (_cache, true) - } else { + else statsClassesReused += 1 - (_cache, false) - } + result } def getMemberMethodCache( methodName: MethodName): MethodCache[List[js.Tree]] = { - _memberMethodCache.getOrElseUpdate(methodName, new MethodCache) + _memberMethodCache.get(methodName) } def getStaticLikeMethodCache(namespace: MemberNamespace, methodName: MethodName): MethodCache[List[js.Tree]] = { - _methodCaches(namespace.ordinal) - .getOrElseUpdate(methodName, new MethodCache) + _methodCaches.get(namespace, methodName) } - def getConstructorCache(): MethodCache[List[js.Tree]] = { - _constructorCache.getOrElse { - val cache = new MethodCache[List[js.Tree]] - _constructorCache = Some(cache) - cache - } - } + def getConstructorCache(): MethodCache[List[js.Tree]] = + _constructorCache.get() def getExportedMemberCache(idx: Int): MethodCache[List[js.Tree]] = - _exportedMembersCache.getOrElseUpdate(idx, new MethodCache) - - def getFullClassChangeTracker(): FullClassChangeTracker = { - _fullClassChangeTracker.getOrElse { - val cache = new FullClassChangeTracker - _fullClassChangeTracker = Some(cache) - cache - } - } - - def cleanAfterRun(): Boolean = { - _methodCaches.foreach(_.filterInPlace((_, c) => c.cleanAfterRun())) - _memberMethodCache.filterInPlace((_, c) => c.cleanAfterRun()) - - if (_constructorCache.exists(!_.cleanAfterRun())) - _constructorCache = None - - _exportedMembersCache.filterInPlace((_, c) => c.cleanAfterRun()) - - if (_fullClassChangeTracker.exists(!_.cleanAfterRun())) - _fullClassChangeTracker = None - - if (!_cacheUsed) - invalidate() - - _methodCaches.exists(_.nonEmpty) || _cacheUsed + _exportedMembersCache.get(idx) + + def getFullClassChangeTracker(): FullClassChangeTracker = + _fullClassChangeTracker.get() + + override def cleanAfterRun(): Boolean = { + val methodCachesUsed = _methodCaches.cleanAfterRun() + val memberMethodCacheUsed = _memberMethodCache.cleanAfterRun() + val constructorCacheUsed = _constructorCache.cleanAfterRun() + val exportedMembersCacheUsed = _exportedMembersCache.cleanAfterRun() + val fullClassChangeTrackerUsed = _fullClassChangeTracker.cleanAfterRun() + + val superCacheUsed = super.cleanAfterRun() + + methodCachesUsed || + memberMethodCacheUsed || + constructorCacheUsed || + exportedMembersCacheUsed || + fullClassChangeTrackerUsed || + superCacheUsed } } - private final class MethodCache[T] extends knowledgeGuardian.KnowledgeAccessor { - private[this] var _tree: WithGlobals[T] = null - private[this] var _lastVersion: Version = Version.Unversioned - private[this] var _cacheUsed = false - - override def invalidate(): Unit = { - super.invalidate() - _tree = null - _lastVersion = Version.Unversioned - } - - def startRun(): Unit = _cacheUsed = false + private final class MethodCache[T] + extends knowledgeGuardian.KnowledgeAccessor + with VersionedCache[WithGlobals[T]] { def getOrElseUpdate(version: Version, v: => WithGlobals[T]): (WithGlobals[T], Boolean) = { - _cacheUsed = true - if (_tree == null || !_lastVersion.sameVersion(version)) { - invalidate() + val result = this.getOrComputeWithChanged(version, v) + if (result._2) statsMethodsInvalidated += 1 - _tree = v - _lastVersion = version - (_tree, true) - } else { + else statsMethodsReused += 1 - (_tree, false) - } - } - - def cleanAfterRun(): Boolean = { - if (!_cacheUsed) - invalidate() - - _cacheUsed + result } } @@ -1004,7 +939,6 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { private[this] var _lastCtor: WithGlobals[List[js.Tree]] = null private[this] var _lastMemberMethods: List[WithGlobals[List[js.Tree]]] = null private[this] var _lastExportedMembers: List[WithGlobals[List[js.Tree]]] = null - private[this] var _trackerUsed = false override def invalidate(): Unit = { super.invalidate() @@ -1014,8 +948,6 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { _lastExportedMembers = null } - def startRun(): Unit = _trackerUsed = false - def trackChanged(version: Version, ctor: WithGlobals[List[js.Tree]], memberMethods: List[WithGlobals[List[js.Tree]]], exportedMembers: List[WithGlobals[List[js.Tree]]]): Boolean = { @@ -1028,7 +960,7 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { } } - _trackerUsed = true + markUsed() val changed = { !version.sameVersion(_lastVersion) || @@ -1049,30 +981,16 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { changed } - - def cleanAfterRun(): Boolean = { - if (!_trackerUsed) - invalidate() - - _trackerUsed - } } - private class CoreJSLibCache extends knowledgeGuardian.KnowledgeAccessor { - private[this] var _lastModuleContext: ModuleContext = _ - private[this] var _lib: WithGlobals[CoreJSLib.Lib[List[js.Tree]]] = _ + private class CoreJSLibCache + extends knowledgeGuardian.KnowledgeAccessor + with InputEqualityCache[ModuleContext, WithGlobals[CoreJSLib.Lib[List[js.Tree]]]] { def build(moduleContext: ModuleContext): WithGlobals[CoreJSLib.Lib[List[js.Tree]]] = { - if (_lib == null || _lastModuleContext != moduleContext) { - _lib = CoreJSLib.build(sjsGen, prePrint(_, 0), moduleContext, this) - _lastModuleContext = moduleContext - } - _lib - } - - override def invalidate(): Unit = { - super.invalidate() - _lib = null + this.getOrCompute(moduleContext, { + CoreJSLib.build(sjsGen, prePrint(_, 0), moduleContext, this) + }) } } } @@ -1193,14 +1111,14 @@ object Emitter { } private final class DesugaredClassCache { - val privateJSFields = new OneTimeCache[WithGlobals[List[js.Tree]]] - val storeJSSuperClass = new OneTimeCache[WithGlobals[List[js.Tree]]] - val instanceTests = new OneTimeCache[WithGlobals[List[js.Tree]]] - val typeData = new InputEqualityCache[Boolean, WithGlobals[List[js.Tree]]] - val setTypeData = new OneTimeCache[List[js.Tree]] - val moduleAccessor = new OneTimeCache[WithGlobals[List[js.Tree]]] - val staticInitialization = new OneTimeCache[List[js.Tree]] - val staticFields = new OneTimeCache[WithGlobals[List[js.Tree]]] + val privateJSFields = new SimpleOneTimeCache[WithGlobals[List[js.Tree]]] + val storeJSSuperClass = new SimpleOneTimeCache[WithGlobals[List[js.Tree]]] + val instanceTests = new SimpleOneTimeCache[WithGlobals[List[js.Tree]]] + val typeData = new SimpleInputEqualityCache[Boolean, WithGlobals[List[js.Tree]]] + val setTypeData = new SimpleOneTimeCache[List[js.Tree]] + val moduleAccessor = new SimpleOneTimeCache[WithGlobals[List[js.Tree]]] + val staticInitialization = new SimpleOneTimeCache[List[js.Tree]] + val staticFields = new SimpleOneTimeCache[WithGlobals[List[js.Tree]]] } private final class GeneratedClass( @@ -1212,33 +1130,6 @@ object Emitter { val changed: Boolean ) - private final class OneTimeCache[A >: Null] { - private[this] var value: A = null - def getOrElseUpdate(v: => A): A = { - if (value == null) - value = v - value - } - } - - /** A cache that depends on an `input: I`, testing with `==`. - * - * @tparam I - * the type of input, for which `==` must meaningful - */ - private final class InputEqualityCache[I, A >: Null] { - private[this] var lastInput: Option[I] = None - private[this] var value: A = null - - def getOrElseUpdate(input: I, v: => A): A = { - if (!lastInput.contains(input)) { - value = v - lastInput = Some(input) - } - value - } - } - private case class ClassID( ancestors: List[ClassName], moduleContext: ModuleContext) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/KnowledgeGuardian.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/KnowledgeGuardian.scala index c7a94abfb9..5aefd37e86 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/KnowledgeGuardian.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/KnowledgeGuardian.scala @@ -24,12 +24,12 @@ import org.scalajs.linker.interface.ModuleKind import org.scalajs.linker.standard._ import org.scalajs.linker.standard.ModuleSet.ModuleID import org.scalajs.linker.CollectionsCompat.MutableMapCompatOps +import org.scalajs.linker.caching +import org.scalajs.linker.caching._ import EmitterNames._ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { - import KnowledgeGuardian._ - private var specialInfo: SpecialInfo = _ private val classes = mutable.Map.empty[ClassName, Class] @@ -112,11 +112,6 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { } } - if (invalidateAll) { - classes.valuesIterator.foreach(_.unregisterAll()) - specialInfo.unregisterAll() - } - invalidateAll } @@ -175,7 +170,9 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { } } - abstract class KnowledgeAccessor extends GlobalKnowledge with Invalidatable { + abstract class KnowledgeAccessor + extends Cache with GlobalKnowledge with caching.KnowledgeAccessor { + /* In theory, a KnowledgeAccessor should *contain* a GlobalKnowledge, not * *be* a GlobalKnowledge. We organize it that way to reduce memory * footprint and pointer indirections. @@ -245,106 +242,45 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { private class Class(initClass: LinkedClass, initHasInlineableInit: Boolean, initStaticFieldMirrors: Map[FieldName, List[String]], - initModule: Option[ModuleID]) - extends Unregisterable { + initModule: Option[ModuleID]) { private val className = initClass.className private var isAlive: Boolean = true - private var isInterface = computeIsInterface(initClass) - private var hasInlineableInit = initHasInlineableInit - private var hasStoredSuperClass = computeHasStoredSuperClass(initClass) - private var hasInstances = initClass.hasInstances - private var jsClassCaptureTypes = computeJSClassCaptureTypes(initClass) - private var jsNativeLoadSpec = computeJSNativeLoadSpec(initClass) - private var jsNativeMemberLoadSpecs = computeJSNativeMemberLoadSpecs(initClass) - private var superClass = computeSuperClass(initClass) - private var fieldDefsVersion = computeFieldDefsVersion(initClass) - private var fieldDefs = computeFieldDefs(initClass) - private var staticFieldMirrors = initStaticFieldMirrors - private var module = initModule - - private val isInterfaceAskers = mutable.Set.empty[Invalidatable] - private val hasInlineableInitAskers = mutable.Set.empty[Invalidatable] - private val hasStoredSuperClassAskers = mutable.Set.empty[Invalidatable] - private val hasInstancesAskers = mutable.Set.empty[Invalidatable] - private val jsClassCaptureTypesAskers = mutable.Set.empty[Invalidatable] - private val jsNativeLoadSpecAskers = mutable.Set.empty[Invalidatable] - private val jsNativeMemberLoadSpecsAskers = mutable.Set.empty[Invalidatable] - private val superClassAskers = mutable.Set.empty[Invalidatable] - private val fieldDefsAskers = mutable.Set.empty[Invalidatable] - private val staticFieldMirrorsAskers = mutable.Set.empty[Invalidatable] - private val moduleAskers = mutable.Set.empty[Invalidatable] + private val isInterface = KnowledgeSource(initClass)(computeIsInterface(_)) + private val hasInlineableInit = KnowledgeSource(initHasInlineableInit)(identity) + private val hasStoredSuperClass = KnowledgeSource(initClass)(computeHasStoredSuperClass(_)) + private val hasInstances = KnowledgeSource(initClass)(_.hasInstances) + private val jsClassCaptureTypes = KnowledgeSource(initClass)(computeJSClassCaptureTypes(_)) + private val jsNativeLoadSpec = KnowledgeSource(initClass)(computeJSNativeLoadSpec(_)) + private val jsNativeMemberLoadSpecs = KnowledgeSource(initClass)(computeJSNativeMemberLoadSpecs(_)) + private val superClass = KnowledgeSource(initClass)(computeSuperClass(_)) + + private val fieldDefs = { + KnowledgeSource.withCustomComparison(initClass)(computeFieldDefsWithVersion(_))( + (a, b) => a._2.sameVersion(b._2)) + } + + private val staticFieldMirrors = KnowledgeSource(initStaticFieldMirrors)(identity) + private val module = KnowledgeSource(initModule)(identity) def update(linkedClass: LinkedClass, newHasInlineableInit: Boolean, newStaticFieldMirrors: Map[FieldName, List[String]], newModule: Option[ModuleID]): Unit = { isAlive = true - val newIsInterface = computeIsInterface(linkedClass) - if (newIsInterface != isInterface) { - isInterface = newIsInterface - invalidateAskers(isInterfaceAskers) - } - - if (newHasInlineableInit != hasInlineableInit) { - hasInlineableInit = newHasInlineableInit - invalidateAskers(hasInlineableInitAskers) - } - - val newHasStoredSuperClass = computeHasStoredSuperClass(linkedClass) - if (newHasStoredSuperClass != hasStoredSuperClass) { - hasStoredSuperClass = newHasStoredSuperClass - invalidateAskers(hasStoredSuperClassAskers) - } - - val newHasInstances = linkedClass.hasInstances - if (newHasInstances != hasInstances) { - hasInstances = newHasInstances - invalidateAskers(hasInstancesAskers) - } - - val newJSClassCaptureTypes = computeJSClassCaptureTypes(linkedClass) - if (newJSClassCaptureTypes != jsClassCaptureTypes) { - jsClassCaptureTypes = newJSClassCaptureTypes - invalidateAskers(jsClassCaptureTypesAskers) - } - - val newJSNativeLoadSpec = computeJSNativeLoadSpec(linkedClass) - if (newJSNativeLoadSpec != jsNativeLoadSpec) { - jsNativeLoadSpec = newJSNativeLoadSpec - invalidateAskers(jsNativeLoadSpecAskers) - } - - val newJSNativeMemberLoadSpecs = computeJSNativeMemberLoadSpecs(linkedClass) - if (newJSNativeMemberLoadSpecs != jsNativeMemberLoadSpecs) { - jsNativeMemberLoadSpecs = newJSNativeMemberLoadSpecs - invalidateAskers(jsNativeMemberLoadSpecsAskers) - } - - val newSuperClass = computeSuperClass(linkedClass) - if (newSuperClass != superClass) { - superClass = newSuperClass - invalidateAskers(superClassAskers) - } - - val newFieldDefsVersion = computeFieldDefsVersion(linkedClass) - if (!newFieldDefsVersion.sameVersion(fieldDefsVersion)) { - fieldDefsVersion = newFieldDefsVersion - fieldDefs = computeFieldDefs(linkedClass) - invalidateAskers(fieldDefsAskers) - } - - if (newStaticFieldMirrors != staticFieldMirrors) { - staticFieldMirrors = newStaticFieldMirrors - invalidateAskers(staticFieldMirrorsAskers) - } - - if (newModule != module) { - module = newModule - invalidateAskers(moduleAskers) - } + isInterface.update(linkedClass) + hasInlineableInit.update(newHasInlineableInit) + hasStoredSuperClass.update(linkedClass) + hasInstances.update(linkedClass) + jsClassCaptureTypes.update(linkedClass) + jsNativeLoadSpec.update(linkedClass) + jsNativeMemberLoadSpecs.update(linkedClass) + superClass.update(linkedClass) + fieldDefs.update(linkedClass) + staticFieldMirrors.update(newStaticFieldMirrors) + module.update(newModule) } private def computeIsInterface(linkedClass: LinkedClass): Boolean = @@ -374,7 +310,7 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { private def computeSuperClass(linkedClass: LinkedClass): ClassName = linkedClass.superClass.fold[ClassName](null.asInstanceOf[ClassName])(_.name) - /** Computes the version of the fields of a `LinkedClass`. + /** Computes the fields of a `LinkedClass` along with a `Version` for them. * * The version is composed of * @@ -390,132 +326,71 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { * We do not try to use the names of `JSFieldDef`s because they are * `Tree`s, which are not efficiently comparable nor versionable here. */ - private def computeFieldDefsVersion(linkedClass: LinkedClass): Version = { - val hasAnyJSField = linkedClass.fields.exists(_.isInstanceOf[JSFieldDef]) + private def computeFieldDefsWithVersion(linkedClass: LinkedClass): (List[AnyFieldDef], Version) = { + val fields = linkedClass.fields + val hasAnyJSField = fields.exists(_.isInstanceOf[JSFieldDef]) val hasAnyJSFieldVersion = Version.fromByte(if (hasAnyJSField) 1 else 0) - val scalaFieldNamesVersion = linkedClass.fields.collect { + val scalaFieldNamesVersion = fields.collect { case FieldDef(_, FieldIdent(name), _, _) => Version.fromUTF8String(name.simpleName.encoded) } - Version.combine((linkedClass.version :: hasAnyJSFieldVersion :: scalaFieldNamesVersion): _*) + val version = + Version.combine((linkedClass.version :: hasAnyJSFieldVersion :: scalaFieldNamesVersion): _*) + (fields, version) } - private def computeFieldDefs(linkedClass: LinkedClass): List[AnyFieldDef] = - linkedClass.fields - def testAndResetIsAlive(): Boolean = { val result = isAlive isAlive = false result } - def askIsInterface(invalidatable: Invalidatable): Boolean = { - invalidatable.registeredTo(this) - isInterfaceAskers += invalidatable - isInterface - } + def askIsInterface(accessor: KnowledgeAccessor): Boolean = + isInterface.askKnowledge(accessor) - def askAllScalaClassFieldDefs(invalidatable: Invalidatable): List[AnyFieldDef] = { - invalidatable.registeredTo(this) - superClassAskers += invalidatable - fieldDefsAskers += invalidatable - val inheritedFieldDefs = - if (superClass == null) Nil - else classes(superClass).askAllScalaClassFieldDefs(invalidatable) - inheritedFieldDefs ::: fieldDefs + def askAllScalaClassFieldDefs(accessor: KnowledgeAccessor): List[AnyFieldDef] = { + val inheritedFieldDefs = superClass.askKnowledge(accessor) match { + case null => Nil + case superClass => classes(superClass).askAllScalaClassFieldDefs(accessor) + } + val myFieldDefs = fieldDefs.askKnowledge(accessor)._1 + inheritedFieldDefs ::: myFieldDefs } - def askHasInlineableInit(invalidatable: Invalidatable): Boolean = { - invalidatable.registeredTo(this) - hasInlineableInitAskers += invalidatable - hasInlineableInit - } + def askHasInlineableInit(accessor: KnowledgeAccessor): Boolean = + hasInlineableInit.askKnowledge(accessor) - def askHasStoredSuperClass(invalidatable: Invalidatable): Boolean = { - invalidatable.registeredTo(this) - hasStoredSuperClassAskers += invalidatable - hasStoredSuperClass - } + def askHasStoredSuperClass(accessor: KnowledgeAccessor): Boolean = + hasStoredSuperClass.askKnowledge(accessor) - def askHasInstances(invalidatable: Invalidatable): Boolean = { - invalidatable.registeredTo(this) - hasInstancesAskers += invalidatable - hasInstances - } + def askHasInstances(accessor: KnowledgeAccessor): Boolean = + hasInstances.askKnowledge(accessor) - def askJSClassCaptureTypes(invalidatable: Invalidatable): Option[List[Type]] = { - invalidatable.registeredTo(this) - jsClassCaptureTypesAskers += invalidatable - jsClassCaptureTypes - } + def askJSClassCaptureTypes(accessor: KnowledgeAccessor): Option[List[Type]] = + jsClassCaptureTypes.askKnowledge(accessor) - def askJSNativeLoadSpec(invalidatable: Invalidatable): Option[JSNativeLoadSpec] = { - invalidatable.registeredTo(this) - jsNativeLoadSpecAskers += invalidatable - jsNativeLoadSpec - } + def askJSNativeLoadSpec(accessor: KnowledgeAccessor): Option[JSNativeLoadSpec] = + jsNativeLoadSpec.askKnowledge(accessor) - def askJSNativeLoadSpec(invalidatable: Invalidatable, member: MethodName): JSNativeLoadSpec = { - invalidatable.registeredTo(this) - jsNativeMemberLoadSpecsAskers += invalidatable - jsNativeMemberLoadSpecs(member) - } + def askJSNativeLoadSpec(accessor: KnowledgeAccessor, member: MethodName): JSNativeLoadSpec = + jsNativeMemberLoadSpecs.askKnowledge(accessor)(member) - def askJSSuperClass(invalidatable: Invalidatable): ClassName = { - invalidatable.registeredTo(this) - superClassAskers += invalidatable - superClass - } + def askJSSuperClass(accessor: KnowledgeAccessor): ClassName = + superClass.askKnowledge(accessor) - def askFieldDefs(invalidatable: Invalidatable): List[AnyFieldDef] = { - invalidatable.registeredTo(this) - fieldDefsAskers += invalidatable - fieldDefs - } + def askFieldDefs(accessor: KnowledgeAccessor): List[AnyFieldDef] = + fieldDefs.askKnowledge(accessor)._1 - def askStaticFieldMirrors(invalidatable: Invalidatable, + def askStaticFieldMirrors(accessor: KnowledgeAccessor, field: FieldName): List[String] = { - invalidatable.registeredTo(this) - staticFieldMirrorsAskers += invalidatable - staticFieldMirrors.getOrElse(field, Nil) + staticFieldMirrors.askKnowledge(accessor).getOrElse(field, Nil) } - def askModule(invalidatable: Invalidatable): ModuleID = { - invalidatable.registeredTo(this) - moduleAskers += invalidatable - module.getOrElse { + def askModule(accessor: KnowledgeAccessor): ModuleID = { + module.askKnowledge(accessor).getOrElse { throw new AssertionError( "trying to get module of abstract class " + className.nameString) } } - - def unregister(invalidatable: Invalidatable): Unit = { - isInterfaceAskers -= invalidatable - hasInlineableInitAskers -= invalidatable - hasStoredSuperClassAskers -= invalidatable - hasInstancesAskers -= invalidatable - jsClassCaptureTypesAskers -= invalidatable - jsNativeLoadSpecAskers -= invalidatable - jsNativeMemberLoadSpecsAskers -= invalidatable - superClassAskers -= invalidatable - fieldDefsAskers -= invalidatable - staticFieldMirrorsAskers -= invalidatable - moduleAskers -= invalidatable - } - - /** Call this when we invalidate all caches. */ - def unregisterAll(): Unit = { - isInterfaceAskers.clear() - hasInlineableInitAskers.clear() - hasStoredSuperClassAskers.clear() - hasInstancesAskers.clear() - jsClassCaptureTypesAskers.clear() - jsNativeLoadSpecAskers.clear() - jsNativeMemberLoadSpecsAskers.clear() - superClassAskers.clear() - fieldDefsAskers.clear() - staticFieldMirrorsAskers.clear() - moduleAskers.clear() - } } private class SpecialInfo(initObjectClass: Option[LinkedClass], @@ -523,33 +398,40 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { initArithmeticExceptionClass: Option[LinkedClass], initIllegalArgumentExceptionClass: Option[LinkedClass], initHijackedClasses: Iterable[LinkedClass], - initGlobalInfo: LinkedGlobalInfo) extends Unregisterable { + initGlobalInfo: LinkedGlobalInfo) { import SpecialInfo._ - private var instantiatedSpecialClassBitSet = { - computeInstantiatedSpecialClassBitSet(initClassClass, - initArithmeticExceptionClass, initIllegalArgumentExceptionClass) + /* Knowledge for isXClassInstantiated -- merged for all X because in + * practice that knowledge is only used by the CoreJSLib. + */ + private val instantiatedSpecialClassBitSet = { + KnowledgeSource(initClassClass, initArithmeticExceptionClass, + initIllegalArgumentExceptionClass)( + computeInstantiatedSpecialClassBitSet(_, _, _)) } private var isParentDataAccessed = computeIsParentDataAccessed(initGlobalInfo) - private var methodsInRepresentativeClasses = - computeMethodsInRepresentativeClasses(initObjectClass, initHijackedClasses) + private val methodsInRepresentativeClasses = { + KnowledgeSource(initObjectClass, initHijackedClasses)( + computeMethodsInRepresentativeClasses(_, _)) + } - private var methodsInObject = - computeMethodsInObject(initObjectClass) + private val methodsInObject = { + /* Usage-sites of methodsInObject never cache. + * Since the comparison is expensive, we do not bother. + * Instead, we always invalidate. + */ + KnowledgeSource.withCustomComparison(initObjectClass)( + computeMethodsInObject(_))( + (a, b) => false) + } private var hijackedDescendants = computeHijackedDescendants(initHijackedClasses) - // Askers of isXClassInstantiated -- merged for all X because in practice that's only the CoreJSLib - private val instantiatedSpecialClassAskers = mutable.Set.empty[Invalidatable] - - private val methodsInRepresentativeClassesAskers = mutable.Set.empty[Invalidatable] - private val methodsInObjectAskers = mutable.Set.empty[Invalidatable] - def update(objectClass: Option[LinkedClass], classClass: Option[LinkedClass], arithmeticExceptionClass: Option[LinkedClass], illegalArgumentExceptionClass: Option[LinkedClass], @@ -557,12 +439,8 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { globalInfo: LinkedGlobalInfo): Boolean = { var invalidateAll = false - val newInstantiatedSpecialClassBitSet = computeInstantiatedSpecialClassBitSet( - classClass, arithmeticExceptionClass, illegalArgumentExceptionClass) - if (newInstantiatedSpecialClassBitSet != instantiatedSpecialClassBitSet) { - instantiatedSpecialClassBitSet = newInstantiatedSpecialClassBitSet - invalidateAskers(instantiatedSpecialClassAskers) - } + instantiatedSpecialClassBitSet.update( + (classClass, arithmeticExceptionClass, illegalArgumentExceptionClass)) val newIsParentDataAccessed = computeIsParentDataAccessed(globalInfo) if (newIsParentDataAccessed != isParentDataAccessed) { @@ -570,19 +448,8 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { invalidateAll = true } - val newMethodsInRepresentativeClasses = - computeMethodsInRepresentativeClasses(objectClass, hijackedClasses) - if (newMethodsInRepresentativeClasses != methodsInRepresentativeClasses) { - methodsInRepresentativeClasses = newMethodsInRepresentativeClasses - invalidateAskers(methodsInRepresentativeClassesAskers) - } - - /* Usage-sites of methodsInObject never cache. - * Therefore, we do not bother comparing (which is expensive), but simply - * invalidate. - */ - methodsInObject = computeMethodsInObject(objectClass) - invalidateAskers(methodsInObjectAskers) + methodsInRepresentativeClasses.update((objectClass, hijackedClasses)) + methodsInObject.update(objectClass) val newHijackedDescendants = computeHijackedDescendants(hijackedClasses) if (newHijackedDescendants != hijackedDescendants) { @@ -658,57 +525,36 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { } } - def askIsClassClassInstantiated(invalidatable: Invalidatable): Boolean = { - invalidatable.registeredTo(this) - instantiatedSpecialClassAskers += invalidatable - (instantiatedSpecialClassBitSet & SpecialClassClass) != 0 + def askIsClassClassInstantiated(accessor: KnowledgeAccessor): Boolean = { + val bitSet = instantiatedSpecialClassBitSet.askKnowledge(accessor) + (bitSet & SpecialClassClass) != 0 } - def askIsArithmeticExceptionClassInstantiatedWithStringArg(invalidatable: Invalidatable): Boolean = { - invalidatable.registeredTo(this) - instantiatedSpecialClassAskers += invalidatable - (instantiatedSpecialClassBitSet & SpecialClassArithmeticExceptionWithStringArg) != 0 + def askIsArithmeticExceptionClassInstantiatedWithStringArg(accessor: KnowledgeAccessor): Boolean = { + val bitSet = instantiatedSpecialClassBitSet.askKnowledge(accessor) + (bitSet & SpecialClassArithmeticExceptionWithStringArg) != 0 } - def askIsIllegalArgumentExceptionClassInstantiatedWithNoArg(invalidatable: Invalidatable): Boolean = { - invalidatable.registeredTo(this) - instantiatedSpecialClassAskers += invalidatable - (instantiatedSpecialClassBitSet & SpecialClassIllegalArgumentExceptionWithNoArg) != 0 + def askIsIllegalArgumentExceptionClassInstantiatedWithNoArg(accessor: KnowledgeAccessor): Boolean = { + val bitSet = instantiatedSpecialClassBitSet.askKnowledge(accessor) + (bitSet & SpecialClassIllegalArgumentExceptionWithNoArg) != 0 } - def askIsParentDataAccessed(invalidatable: Invalidatable): Boolean = + def askIsParentDataAccessed(accessor: KnowledgeAccessor): Boolean = isParentDataAccessed def askMethodsInRepresentativeClasses( - invalidatable: Invalidatable): List[(MethodName, Set[ClassName])] = { - invalidatable.registeredTo(this) - methodsInRepresentativeClassesAskers += invalidatable - methodsInRepresentativeClasses + accessor: KnowledgeAccessor): List[(MethodName, Set[ClassName])] = { + methodsInRepresentativeClasses.askKnowledge(accessor) } - def askMethodsInObject(invalidatable: Invalidatable): List[MethodDef] = { - invalidatable.registeredTo(this) - methodsInObjectAskers += invalidatable - methodsInObject - } + def askMethodsInObject(accessor: KnowledgeAccessor): List[MethodDef] = + methodsInObject.askKnowledge(accessor) def askHijackedDescendants( - invalidatable: Invalidatable): Map[ClassName, Set[ClassName]] = { + accessor: KnowledgeAccessor): Map[ClassName, Set[ClassName]] = { hijackedDescendants } - - def unregister(invalidatable: Invalidatable): Unit = { - instantiatedSpecialClassAskers -= invalidatable - methodsInRepresentativeClassesAskers -= invalidatable - methodsInObjectAskers -= invalidatable - } - - /** Call this when we invalidate all caches. */ - def unregisterAll(): Unit = { - instantiatedSpecialClassAskers.clear() - methodsInRepresentativeClassesAskers.clear() - methodsInObjectAskers.clear() - } } private object SpecialInfo { @@ -716,39 +562,4 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { private final val SpecialClassArithmeticExceptionWithStringArg = 1 << 1 private final val SpecialClassIllegalArgumentExceptionWithNoArg = 1 << 2 } - - private def invalidateAskers(askers: mutable.Set[Invalidatable]): Unit = { - /* Calling `invalidate` cause the `Invalidatable` to call `unregister()` in - * this class, which will mutate the `askers` set. Therefore, we cannot - * directly iterate over `askers`, and need to take a snapshot instead. - */ - val snapshot = askers.toSeq - askers.clear() - snapshot.foreach(_.invalidate()) - } -} - -private[emitter] object KnowledgeGuardian { - private trait Unregisterable { - def unregister(invalidatable: Invalidatable): Unit - } - - trait Invalidatable { - private val _registeredTo = mutable.Set.empty[Unregisterable] - - private[KnowledgeGuardian] def registeredTo( - unregisterable: Unregisterable): Unit = { - _registeredTo += unregisterable - } - - /** To be overridden to perform subclass-specific invalidation. - * - * All overrides should call the default implementation with `super` so - * that this `Invalidatable` is unregistered from the dependency graph. - */ - def invalidate(): Unit = { - _registeredTo.foreach(_.unregister(this)) - _registeredTo.clear() - } - } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/Cache.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/Cache.scala new file mode 100644 index 0000000000..ad2f40bc11 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/Cache.scala @@ -0,0 +1,38 @@ +/* + * 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.caching + +/** Base class of all caches. + * + * A cache can be invalidated to clear everything it cached. + * + * Each cache keeps track of whether it was *used* in any given run. + * `cleanAfterRun()` invalidates the cache if it was not used. Then it resets + * the tracker to prepare for the next run. + */ +abstract class Cache { + private var _cacheUsed: Boolean = false + + protected[caching] def markUsed(): Unit = + _cacheUsed = true + + def invalidate(): Unit = () + + def cleanAfterRun(): Boolean = { + val wasUsed = _cacheUsed + if (!wasUsed) + invalidate() + _cacheUsed = false + wasUsed + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/CacheAggregate.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/CacheAggregate.scala new file mode 100644 index 0000000000..77b7282463 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/CacheAggregate.scala @@ -0,0 +1,21 @@ +/* + * 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.caching + +/** Marker trait for caches that aggregate subcaches. + * + * This trait is for documentation purposes only. Cache aggregates *own* their + * subcaches. The aggregate's `cleanAfterRun()` method calls the same method + * its subcaches. It may discard subcaches that were not used in the run. + */ +trait CacheAggregate extends Cache diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/CacheMap.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/CacheMap.scala new file mode 100644 index 0000000000..c266ead78d --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/CacheMap.scala @@ -0,0 +1,56 @@ +/* + * 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.caching + +/** A map of subcaches. + * + * A cache map is like a `HashMap` with auto-computed values. Values must be + * caches themselves. + * + * `CacheMap` itself is not thread-safe. Use [[ConcurrentCacheMap]] if several + * threads must concurrently call `get()`. + * + * `CacheMap` has a single abstract method `createValue`. It is designed to + * be constructible as a SAM lambda. + */ +abstract class CacheMap[Key, Value <: Cache] extends Cache with CacheAggregate { + private val _caches: java.util.Map[Key, Value] = createUnderlyingHashMap() + + protected def createUnderlyingHashMap(): java.util.Map[Key, Value] = + new java.util.HashMap() + + protected def createValue(key: Key): Value + + /** Unique instance of the lambda that we pass to `computeIfAbsent`. */ + private val createValueFunction: java.util.function.Function[Key, Value] = + (key: Key) => createValue(key) + + override def invalidate(): Unit = { + super.invalidate() + _caches.clear() // TODO do we need to invalidate all subcaches? + } + + def get(key: Key): Value = { + markUsed() + val result = _caches.computeIfAbsent(key, createValueFunction) + result.markUsed() + result + } + + override def cleanAfterRun(): Boolean = { + val result = super.cleanAfterRun() + if (result) + _caches.entrySet().removeIf(!_.getValue().cleanAfterRun()) + result + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/CacheOption.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/CacheOption.scala new file mode 100644 index 0000000000..42fa949bef --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/CacheOption.scala @@ -0,0 +1,51 @@ +/* + * 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.caching + +/** An optional subcache. + * + * Shallow shell around another cache. It discards the instance of the + * underlying cache when the latter was not used in a run. + * + * `CacheOption` has a single abstract method `createValue`. It is designed + * to be constructible as a SAM lambda. + */ +abstract class CacheOption[Value <: Cache] extends Cache with CacheAggregate { + private var initialized: Boolean = false + private var underlying: Value = null.asInstanceOf[Value] + + protected def createValue(): Value + + override def invalidate(): Unit = { + super.invalidate() + initialized = false + underlying = null.asInstanceOf[Value] // TODO do we need to invalidate the subcache? + } + + def get(): Value = { + markUsed() + if (!initialized) { + underlying = createValue() + initialized = true + } + underlying.markUsed() + underlying + } + + override def cleanAfterRun(): Boolean = { + val result = super.cleanAfterRun() + if (result && !underlying.cleanAfterRun()) + invalidate() + result + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/ConcurrentCacheMap.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/ConcurrentCacheMap.scala new file mode 100644 index 0000000000..65d79ed678 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/ConcurrentCacheMap.scala @@ -0,0 +1,29 @@ +/* + * 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.caching + +/** A concurrent map of subcaches. + * + * A concurrent cache map is a [[CacheMap]] on which concurrent calls to `get` + * are allowed (even for the same key). + * + * `cleanAfterRun()` is not thread-safe. There must exist a happens-before + * relationship between any call to `cleanAfterRun()` and other methods. + * + * `ConcurrentCacheMap` has a single abstract method `initialValue`. It is + * designed to be constructible as a SAM lambda. + */ +abstract class ConcurrentCacheMap[Key, Value <: Cache] extends CacheMap[Key, Value] { + override protected def createUnderlyingHashMap(): java.util.Map[Key, Value] = + new java.util.concurrent.ConcurrentHashMap() +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/InputEqualityCache.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/InputEqualityCache.scala new file mode 100644 index 0000000000..6f7c78a3c1 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/InputEqualityCache.scala @@ -0,0 +1,43 @@ +/* + * 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.caching + +/** A cache that depends on an `input: I`, testing with `==`. + * + * On first request, or when the input changes, the value is recomputed. + * + * @tparam I + * the type of input, for which `==` must be meaningful + */ +trait InputEqualityCache[I, A] extends Cache { + private var initialized: Boolean = false + private var lastInput: I = null.asInstanceOf[I] + private var value: A = null.asInstanceOf[A] + + override def invalidate(): Unit = { + super.invalidate() + initialized = false + lastInput = null.asInstanceOf[I] + value = null.asInstanceOf[A] + } + + protected final def getOrCompute(input: I, v: => A): A = { + markUsed() + if (!initialized || input != lastInput) { + value = v + lastInput = input + initialized = true + } + value + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/KnowledgeAccessor.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/KnowledgeAccessor.scala new file mode 100644 index 0000000000..d850a2e5de --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/KnowledgeAccessor.scala @@ -0,0 +1,28 @@ +/* + * 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.caching + +import scala.collection.mutable + +trait KnowledgeAccessor extends Cache { + private val _registeredTo = mutable.HashSet.empty[KnowledgeSource[_, _]] + + private[caching] def registeredTo(source: KnowledgeSource[_, _]): Unit = + _registeredTo += source + + override def invalidate(): Unit = { + super.invalidate() + _registeredTo.foreach(_.unregister(this)) + _registeredTo.clear() + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/KnowledgeSource.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/KnowledgeSource.scala new file mode 100644 index 0000000000..05259a308e --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/KnowledgeSource.scala @@ -0,0 +1,89 @@ +/* + * 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.caching + +import scala.collection.mutable + +abstract class KnowledgeSource[I, A](initInput: I) { + private var knowledge = compute(initInput) + private val askers = mutable.HashSet.empty[KnowledgeAccessor] + + private[caching] def unregister(accessor: KnowledgeAccessor): Unit = + askers -= accessor + + protected def compute(input: I): A + + def update(input: I): Unit = { + val newKnowledge = compute(input) + if (!sameKnowledge(newKnowledge, knowledge)) { + knowledge = newKnowledge + invalidateAskers() + } + } + + protected def sameKnowledge(a: A, b: A): Boolean = + a == b + + private def invalidateAskers(): Unit = { + /* Calling `invalidate` causes the `KnowledgeAccessor` to call + * `unregister()` in this class, which will mutate the `askers` set. + * Therefore, we cannot directly iterate over `askers`, and need to take a + * snapshot instead. + */ + val snapshot = askers.toSeq + askers.clear() + snapshot.foreach(_.invalidate()) + } + + def askKnowledge(accessor: KnowledgeAccessor): A = { + if (askers.add(accessor)) + accessor.registeredTo(this) + knowledge + } +} + +object KnowledgeSource { + def apply[I, A](initInput: I)(computeFun: I => A): KnowledgeSource[I, A] = { + new KnowledgeSource[I, A](initInput) { + protected def compute(input: I): A = + computeFun(input) + } + } + + def apply[I1, I2, A](initInput1: I1, initInput2: I2)( + computeFun: (I1, I2) => A): KnowledgeSource[(I1, I2), A] = { + new KnowledgeSource[(I1, I2), A]((initInput1, initInput2)) { + protected def compute(input: (I1, I2)): A = + computeFun(input._1, input._2) + } + } + + def apply[I1, I2, I3, A](initInput1: I1, initInput2: I2, initInput3: I3)( + computeFun: (I1, I2, I3) => A): KnowledgeSource[(I1, I2, I3), A] = { + new KnowledgeSource[(I1, I2, I3), A]((initInput1, initInput2, initInput3)) { + protected def compute(input: (I1, I2, I3)): A = + computeFun(input._1, input._2, input._3) + } + } + + def withCustomComparison[I, A](initInput: I)(computeFun: I => A)( + sameKnowledgeFun: (A, A) => Boolean): KnowledgeSource[I, A] = { + new KnowledgeSource[I, A](initInput) { + protected def compute(input: I): A = + computeFun(input) + + override protected def sameKnowledge(a: A, b: A): Boolean = + sameKnowledgeFun(a, b) + } + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/NamespacedMethodCacheMap.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/NamespacedMethodCacheMap.scala new file mode 100644 index 0000000000..faac46eba9 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/NamespacedMethodCacheMap.scala @@ -0,0 +1,53 @@ +/* + * 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.caching + +import org.scalajs.ir.Names._ +import org.scalajs.ir.Trees.MemberNamespace + +/** A cache map specialized for keys that are pairs `(MemberNamespace, MethodName)`. + * + * This class follows the same contract as [[CacheMap]]. + */ +abstract class NamespacedMethodCacheMap[Value <: Cache] extends Cache with CacheAggregate { + private val _caches: Array[java.util.Map[MethodName, Value]] = + Array.fill(MemberNamespace.Count)(createUnderlyingHashMap()) + + protected def createUnderlyingHashMap(): java.util.Map[MethodName, Value] = + new java.util.HashMap() + + protected def createValue(methodName: MethodName): Value + + /** Unique instance of the lambda that we pass to `computeIfAbsent`. */ + private val createValueFunction: java.util.function.Function[MethodName, Value] = + (methodName: MethodName) => createValue(methodName) + + override def invalidate(): Unit = { + super.invalidate() + _caches.foreach(_.clear()) // TODO do we need to invalidate all subcaches? + } + + def get(namespace: MemberNamespace, methodName: MethodName): Value = { + markUsed() + val result = _caches(namespace.ordinal).computeIfAbsent(methodName, createValueFunction) + result.markUsed() + result + } + + override def cleanAfterRun(): Boolean = { + val result = super.cleanAfterRun() + if (result) + _caches.foreach(_.entrySet().removeIf(!_.getValue().cleanAfterRun())) + result + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/OneTimeCache.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/OneTimeCache.scala new file mode 100644 index 0000000000..d4dccf4d93 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/OneTimeCache.scala @@ -0,0 +1,34 @@ +/* + * 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.caching + +/** Cache that holds a single value, computed the first time it is requested. */ +trait OneTimeCache[A] extends Cache { + private var initialized: Boolean = false + private var value: A = null.asInstanceOf[A] + + override def invalidate(): Unit = { + super.invalidate() + initialized = false + value = null.asInstanceOf[A] + } + + protected final def getOrCompute(v: => A): A = { + markUsed() + if (!initialized) { + value = v + initialized = true + } + value + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleInputEqualityCache.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleInputEqualityCache.scala new file mode 100644 index 0000000000..2bde30d956 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleInputEqualityCache.scala @@ -0,0 +1,18 @@ +/* + * 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.caching + +final class SimpleInputEqualityCache[I, A] extends InputEqualityCache[I, A] { + def getOrElseUpdate(input: I, v: => A): A = + getOrCompute(input, v) +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleOneTimeCache.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleOneTimeCache.scala new file mode 100644 index 0000000000..19e4a1f200 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleOneTimeCache.scala @@ -0,0 +1,19 @@ +/* + * 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.caching + +/** Cache that holds a single value, computed the first time it is requested. */ +final class SimpleOneTimeCache[A] extends OneTimeCache[A] { + def getOrElseUpdate(v: => A): A = + getOrCompute(v) +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleVersionedCache.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleVersionedCache.scala new file mode 100644 index 0000000000..c9f758e316 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleVersionedCache.scala @@ -0,0 +1,24 @@ +/* + * 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.caching + +import org.scalajs.ir.Version + +/** A cache for a single value that gets invalidated based on a `Version`. */ +class SimpleVersionedCache[T] extends VersionedCache[T] { + final def getOrElseUpdate(version: Version, computeValue: => T): T = + getOrCompute(version, computeValue) + + final def getOrElseUpdateWithChanged(version: Version, computeValue: => T): (T, Boolean) = + getOrComputeWithChanged(version, computeValue) +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/caching/VersionedCache.scala b/linker/shared/src/main/scala/org/scalajs/linker/caching/VersionedCache.scala new file mode 100644 index 0000000000..33aa0ffffa --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/caching/VersionedCache.scala @@ -0,0 +1,53 @@ +/* + * 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.caching + +import org.scalajs.ir.Version + +/** A cache for a single value that gets invalidated based on a `Version`. */ +trait VersionedCache[T] extends Cache { + private var _lastVersion: Version = Version.Unversioned + private var _value: T = null.asInstanceOf[T] + + override def invalidate(): Unit = { + super.invalidate() + _lastVersion = Version.Unversioned + _value = null.asInstanceOf[T] + } + + private def updateVersion(version: Version): Boolean = { + markUsed() + if (_lastVersion.sameVersion(version)) { + false + } else { + invalidate() + _lastVersion = version + true + } + } + + protected final def getOrCompute(version: Version, computeValue: => T): T = { + if (updateVersion(version)) + _value = computeValue + _value + } + + protected final def getOrComputeWithChanged(version: Version, computeValue: => T): (T, Boolean) = { + if (updateVersion(version)) { + _value = computeValue + (_value, true) + } else { + (_value, false) + } + } +}