From 84f3866b0cff1aa056190172e68f248d865de8fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 2 Jan 2025 12:52:12 +0100 Subject: [PATCH 1/2] Introduce common caching utilities. Previously, we had several reimplementations of the same basic caching mechanisms. In particular, `cleanAfterRun()`-based removal of caches not used in a given run. In this commit, we introduce common caching utilities. The provide common implementations of the various idioms that we use. This simplifies all the use sites, which can now focus on their core logic, instead of mixing it with caching mechanisms. The abstraction is not zero-cost everywhere. It may introduce some constant overhead. --- .../linker/backend/BasicLinkerBackend.scala | 36 +-- .../linker/backend/emitter/Emitter.scala | 265 ++++++------------ .../backend/emitter/KnowledgeGuardian.scala | 8 +- .../org/scalajs/linker/caching/Cache.scala | 38 +++ .../linker/caching/CacheAggregate.scala | 21 ++ .../org/scalajs/linker/caching/CacheMap.scala | 56 ++++ .../scalajs/linker/caching/CacheOption.scala | 51 ++++ .../linker/caching/ConcurrentCacheMap.scala | 29 ++ .../linker/caching/InputEqualityCache.scala | 43 +++ .../caching/NamespacedMethodCacheMap.scala | 53 ++++ .../scalajs/linker/caching/OneTimeCache.scala | 34 +++ .../caching/SimpleInputEqualityCache.scala | 18 ++ .../linker/caching/SimpleOneTimeCache.scala | 19 ++ .../linker/caching/SimpleVersionedCache.scala | 24 ++ .../linker/caching/VersionedCache.scala | 53 ++++ 15 files changed, 530 insertions(+), 218 deletions(-) create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/Cache.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/CacheAggregate.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/CacheMap.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/CacheOption.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/ConcurrentCacheMap.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/InputEqualityCache.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/NamespacedMethodCacheMap.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/OneTimeCache.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleInputEqualityCache.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleOneTimeCache.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/SimpleVersionedCache.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/VersionedCache.scala 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..967dba86ef 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,6 +24,7 @@ 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 EmitterNames._ @@ -175,7 +176,7 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { } } - abstract class KnowledgeAccessor extends GlobalKnowledge with Invalidatable { + abstract class KnowledgeAccessor extends Cache with GlobalKnowledge with Invalidatable { /* In theory, a KnowledgeAccessor should *contain* a GlobalKnowledge, not * *be* a GlobalKnowledge. We organize it that way to reduce memory * footprint and pointer indirections. @@ -733,7 +734,7 @@ private[emitter] object KnowledgeGuardian { def unregister(invalidatable: Invalidatable): Unit } - trait Invalidatable { + trait Invalidatable extends Cache { private val _registeredTo = mutable.Set.empty[Unregisterable] private[KnowledgeGuardian] def registeredTo( @@ -746,7 +747,8 @@ private[emitter] object KnowledgeGuardian { * All overrides should call the default implementation with `super` so * that this `Invalidatable` is unregistered from the dependency graph. */ - def invalidate(): Unit = { + override def invalidate(): Unit = { + super.invalidate() _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/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) + } + } +} From feac5c1f9fe10910a2724077a48d80e808c21648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 2 Jan 2025 17:06:48 +0100 Subject: [PATCH 2/2] Extract the notion of `KnowledgeAccessor` and `KnowledgeSource`. This simplifies the emitter's `KnowledgeGuardian`, which can now focus on its core logic without handling registrations and invalidations. --- .../backend/emitter/KnowledgeGuardian.scala | 409 +++++------------- .../linker/caching/KnowledgeAccessor.scala | 28 ++ .../linker/caching/KnowledgeSource.scala | 89 ++++ 3 files changed, 226 insertions(+), 300 deletions(-) create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/KnowledgeAccessor.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/caching/KnowledgeSource.scala 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 967dba86ef..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,13 +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] @@ -113,11 +112,6 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { } } - if (invalidateAll) { - classes.valuesIterator.foreach(_.unregisterAll()) - specialInfo.unregisterAll() - } - invalidateAll } @@ -176,7 +170,9 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { } } - abstract class KnowledgeAccessor extends Cache with 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. @@ -246,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 = @@ -375,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 * @@ -391,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], @@ -524,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], @@ -558,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) { @@ -571,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) { @@ -659,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 { @@ -717,40 +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 extends Cache { - 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. - */ - override def invalidate(): Unit = { - super.invalidate() - _registeredTo.foreach(_.unregister(this)) - _registeredTo.clear() - } - } } 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) + } + } +}