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) + } + } +}