From 61122d86a26cd132075602c116f78e910d4211a9 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] Refactor: 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 | 258 ++++++------------ .../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, 531 insertions(+), 210 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 b625c51c12..9a95675d68 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 @@ -28,7 +26,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._ @@ -72,9 +70,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) @@ -82,7 +82,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 @@ -151,12 +151,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 { @@ -170,8 +167,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() } } @@ -242,7 +239,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) @@ -426,8 +423,8 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { val className = linkedClass_!.className val ancestors = linkedClass_!.ancestors - val classCache = classCaches.getOrElseUpdate( - new ClassID(kind, ancestors, moduleContext), new ClassCache) + val classCache = + classCaches.get(new ClassID(kind, ancestors, moduleContext)) var changed = false def extractChanged[T](x: (T, Boolean)): T = { @@ -817,8 +814,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 @@ -849,7 +844,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 @@ -865,7 +860,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 @@ -906,7 +901,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 @@ -916,65 +911,47 @@ 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] var _uncachedDecisions: UncachedDecisions = UncachedDecisions.Invalid - 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 _staticLikeMethodsTracker: Option[List[List[js.Tree]]] = None - private[this] var _fullClassChangeTracker: Option[FullClassChangeTracker] = None + + private[this] val _fullClassChangeTracker: CacheOption[FullClassChangeTracker] = + () => new FullClassChangeTracker() override def invalidate(): Unit = { - /* Do not invalidate contained methods, as they have their own - * invalidation logic. - */ super.invalidate() - _cache = null - _lastVersion = Version.Unversioned - _uncachedDecisions = UncachedDecisions.Invalid - } - def startRun(): Unit = { - _cacheUsed = false - _methodCaches.foreach(_.valuesIterator.foreach(_.startRun())) - _memberMethodCache.valuesIterator.foreach(_.startRun()) - _constructorCache.foreach(_.startRun()) - _fullClassChangeTracker.foreach(_.startRun()) + /* The nested Caches have their own invalidation trackers. + * The only thing we need to invalidate here is the uncachedDecisions. + */ + _uncachedDecisions = UncachedDecisions.Invalid } 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 getUncachedDecisions(linkedClass: LinkedClass): (UncachedDecisions, Boolean) = { @@ -995,25 +972,19 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { 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) + _exportedMembersCache.get(idx) /** Track changes to the generated list of static-like methods. * @@ -1028,66 +999,39 @@ final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { } } - 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()) + def getFullClassChangeTracker(): FullClassChangeTracker = + _fullClassChangeTracker.get() - if (_constructorCache.exists(!_.cleanAfterRun())) - _constructorCache = None + override def cleanAfterRun(): Boolean = { + val methodCachesUsed = _methodCaches.cleanAfterRun() + val memberMethodCacheUsed = _memberMethodCache.cleanAfterRun() + val constructorCacheUsed = _constructorCache.cleanAfterRun() + val exportedMembersCacheUsed = _exportedMembersCache.cleanAfterRun() + val fullClassChangeTrackerUsed = _fullClassChangeTracker.cleanAfterRun() - _exportedMembersCache.filterInPlace((_, c) => c.cleanAfterRun()) + val superCacheUsed = super.cleanAfterRun() - if (_fullClassChangeTracker.exists(!_.cleanAfterRun())) - _fullClassChangeTracker = None - - if (!_cacheUsed) - invalidate() - - _methodCaches.exists(_.nonEmpty) || _cacheUsed + 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 } } @@ -1096,7 +1040,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() @@ -1106,13 +1049,11 @@ 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 = { - _trackerUsed = true + markUsed() val changed = { !version.sameVersion(_lastVersion) || @@ -1133,30 +1074,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) + }) } } } @@ -1327,14 +1254,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( @@ -1346,33 +1273,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( kind: ClassKind, 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 349dc2b9af..2d17fedb80 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 @@ -25,6 +25,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._ @@ -176,7 +177,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. @@ -734,7 +735,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( @@ -747,7 +748,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) + } + } +}