Skip to content

Commit b186b9f

Browse files
committed
Improve implementation of classloader caching
- Fix logic error in reference counted closing - Add a new policy to cache regardless of timestamps and regardless of directory elements in the classpath.
1 parent 63e8031 commit b186b9f

File tree

3 files changed

+96
-68
lines changed

3 files changed

+96
-68
lines changed

src/compiler/scala/tools/nsc/classpath/ZipAndJarFileLookupFactory.scala

+70-38
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import scala.reflect.io.{AbstractFile, FileZipArchive, ManifestResources}
2424
import scala.tools.nsc.util.{ClassPath, ClassRepresentation}
2525
import scala.tools.nsc.{CloseableRegistry, Settings}
2626
import FileUtils._
27+
import scala.tools.nsc.io.Jar
2728

2829
/**
2930
* A trait providing an optional cache for classpath entries obtained from zip and jar files.
@@ -39,7 +40,7 @@ sealed trait ZipAndJarFileLookupFactory {
3940
closeableRegistry.registerClosable(result)
4041
result
4142
} else {
42-
cache.getOrCreate(List(zipFile.file.toPath), () => createForZipFile(zipFile, settings.releaseValue), closeableRegistry)
43+
cache.getOrCreate(List(zipFile.file.toPath), () => createForZipFile(zipFile, settings.releaseValue), closeableRegistry, checkStamps = true)
4344
}
4445
}
4546

@@ -195,25 +196,55 @@ final class FileBasedCache[T] {
195196
private case class Stamp(lastModified: FileTime, fileKey: Object)
196197
private case class Entry(stamps: Seq[Stamp], t: T) {
197198
val referenceCount: AtomicInteger = new AtomicInteger(1)
198-
def referenceCountDecrementer: Closeable = new Closeable {
199-
var closed = false
200-
override def close(): Unit = {
201-
if (!closed) {
202-
closed = true
203-
val count = referenceCount.decrementAndGet()
204-
if (count == 0) {
205-
t match {
206-
case cl: Closeable => FileBasedCache.deferredClose(referenceCount, cl)
207-
case _ =>
208-
}
199+
}
200+
private val cache = collection.mutable.Map.empty[Seq[Path], Entry]
201+
202+
private def referenceCountDecrementer(e: Entry, paths: Seq[Path]): Closeable = new Closeable {
203+
var closed = false
204+
override def close(): Unit = {
205+
if (!closed) {
206+
closed = true
207+
val count = e.referenceCount.decrementAndGet()
208+
if (count == 0) {
209+
e.t match {
210+
case cl: Closeable =>
211+
FileBasedCache.timer match {
212+
case Some(timer) =>
213+
val task = new TimerTask {
214+
override def run(): Unit = {
215+
cache.synchronized {
216+
if (e.referenceCount.compareAndSet(0, -1)) {
217+
cache.remove(paths)
218+
cl.close()
219+
}
220+
}
221+
}
222+
}
223+
timer.schedule(task, FileBasedCache.deferCloseMs.toLong)
224+
case None =>
225+
cl.close()
226+
}
227+
case _ =>
209228
}
210229
}
211230
}
212231
}
213232
}
214-
private val cache = collection.mutable.Map.empty[Seq[Path], Entry]
215233

216-
def getOrCreate(paths: Seq[Path], create: () => T, closeableRegistry: CloseableRegistry): T = cache.synchronized {
234+
def checkCacheability(urls: Seq[URL], checkStamps: Boolean, disableCache: Boolean): Either[String, Seq[java.nio.file.Path]] = {
235+
import scala.reflect.io.{AbstractFile, Path}
236+
val urlsAndFiles = urls.filterNot(_.getProtocol == "jrt").map(u => u -> AbstractFile.getURL(u))
237+
val nonJarZips = urlsAndFiles.filter(_._2 == null).filter(p => Jar.isJarOrZip(p._2.file))
238+
def paths = urlsAndFiles.map(t => Path(t._2.file).jfile.toPath)
239+
if (!checkStamps) Right(paths)
240+
else if (disableCache) Left("caching is disabled due to a policy setting")
241+
else {
242+
if (nonJarZips.nonEmpty) Left(s"caching is disabled because of the following classpath elements: ${nonJarZips.map(_._1).mkString(", ")}.")
243+
else Right(paths)
244+
}
245+
}
246+
247+
def getOrCreate(paths: Seq[Path], create: () => T, closeableRegistry: CloseableRegistry, checkStamps: Boolean): T = cache.synchronized {
217248
val stamps = paths.map { path =>
218249
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
219250
val lastModified = attrs.lastModifiedTime()
@@ -223,15 +254,35 @@ final class FileBasedCache[T] {
223254
}
224255

225256
cache.get(paths) match {
226-
case Some(e@Entry(cachedStamps, cached)) if cachedStamps == stamps =>
227-
e.referenceCount.incrementAndGet()
228-
closeableRegistry.registerClosable(e.referenceCountDecrementer)
229-
cached
257+
case Some(e@Entry(cachedStamps, cached)) =>
258+
if (!checkStamps || cachedStamps == stamps) {
259+
// Cache hit
260+
val count = e.referenceCount.incrementAndGet()
261+
assert(count > 0, (stamps, count))
262+
closeableRegistry.registerClosable(referenceCountDecrementer(e, paths))
263+
cached
264+
} else {
265+
// Cache miss: we found an entry but the underlying files have been modified
266+
cached match {
267+
case c: Closeable =>
268+
if (e.referenceCount.get() == 0) {
269+
c.close()
270+
} else {
271+
// TODO: What do do here? Maybe add to a list of closeables polled by a cleanup thread?
272+
}
273+
}
274+
val value = create()
275+
val entry = Entry(stamps, value)
276+
cache.put(paths, entry)
277+
closeableRegistry.registerClosable(referenceCountDecrementer(entry, paths))
278+
value
279+
}
230280
case _ =>
281+
// Cache miss
231282
val value = create()
232283
val entry = Entry(stamps, value)
233284
cache.put(paths, entry)
234-
closeableRegistry.registerClosable(entry.referenceCountDecrementer)
285+
closeableRegistry.registerClosable(referenceCountDecrementer(entry, paths))
235286
value
236287
}
237288
}
@@ -244,29 +295,10 @@ final class FileBasedCache[T] {
244295
}
245296

246297
object FileBasedCache {
247-
// The tension here is that too long a delay could lead to an error (on Windows) with an inability
248-
// to overwrite the JAR. To short a delay and the entry could be evicted before a subsequent
249-
// sub-project compilation is able to get a cache hit. A more comprehensive solution would be to
250-
// involve build tools in the policy: they could close entries with refcount of zero when that
251-
// entry's JAR is about to be overwritten.
252298
private val deferCloseMs = Integer.getInteger("scalac.filebasedcache.defer.close.ms", 1000)
253299
private val timer: Option[Timer] = {
254300
if (deferCloseMs > 0)
255301
Some(new java.util.Timer(true))
256302
else None
257303
}
258-
private def deferredClose(referenceCount: AtomicInteger, closable: Closeable): Unit = {
259-
timer match {
260-
case Some(timer) =>
261-
val task = new TimerTask {
262-
override def run(): Unit = {
263-
if (referenceCount.get == 0)
264-
closable.close()
265-
}
266-
}
267-
timer.schedule(task, FileBasedCache.deferCloseMs.toLong)
268-
case None =>
269-
closable.close()
270-
}
271-
}
272304
}

src/compiler/scala/tools/nsc/plugins/Plugins.scala

+24-29
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ trait Plugins { global: Global =>
6969
* @return
7070
*/
7171
protected def findPluginClassLoader(classpath: Seq[Path]): ClassLoader = {
72-
val disableCache = settings.YcachePluginClassLoader.value == settings.CachePolicy.None.name
72+
val policy = settings.YcachePluginClassLoader.value
73+
val disableCache = policy == settings.CachePolicy.None.name
7374
def newLoader = () => {
7475
val compilerLoader = classOf[Plugin].getClassLoader
7576
val urls = classpath map (_.toURL)
@@ -84,11 +85,16 @@ trait Plugins { global: Global =>
8485
// mitigate the cost of dynamic classloading as it has been
8586
// measured in https://github.com/scala/scala-dev/issues/458.
8687

87-
if (disableCache || classpath.exists(!Jar.isJarOrZip(_))) {
88-
val loader = newLoader()
89-
closeableRegistry.registerClosable(loader)
90-
loader
91-
} else pluginClassLoadersCache.getOrCreate(classpath.map(_.jfile.toPath()), newLoader, closeableRegistry)
88+
val cache = pluginClassLoadersCache
89+
val checkStamps = policy == settings.CachePolicy.LastModified
90+
cache.checkCacheability(classpath.map(_.toURL), checkStamps, disableCache) match {
91+
case Left(msg) =>
92+
val loader = newLoader()
93+
closeableRegistry.registerClosable(loader)
94+
loader
95+
case Right(paths) =>
96+
cache.getOrCreate(classpath.map(_.jfile.toPath()), newLoader, closeableRegistry, checkStamps)
97+
}
9298
}
9399

94100
protected lazy val roughPluginsList: List[Plugin] = loadRoughPluginsList()
@@ -181,29 +187,18 @@ trait Plugins { global: Global =>
181187
ScalaClassLoader.fromURLs(classpath, getClass.getClassLoader)
182188
}
183189

184-
val disableCache = settings.YcacheMacroClassLoader.value == settings.CachePolicy.None.name
185-
if (disableCache) newLoader()
186-
else {
187-
import scala.tools.nsc.io.Jar
188-
import scala.reflect.io.{AbstractFile, Path}
189-
190-
val urlsAndFiles = classpath.map(u => u -> AbstractFile.getURL(u))
191-
val hasNullURL = urlsAndFiles.filter(_._2 eq null)
192-
if (hasNullURL.nonEmpty) {
193-
// TODO if the only null is jrt:// we can still cache
194-
// TODO filter out classpath elements pointing to non-existing files before we get here, that's another source of null
195-
analyzer.macroLogVerbose(s"macro classloader: caching is disabled because `AbstractFile.getURL` returned `null` for ${hasNullURL.map(_._1).mkString(", ")}.")
196-
perRunCaches.recordClassloader(newLoader())
197-
} else {
198-
val locations = urlsAndFiles.map(t => Path(t._2.file))
199-
val nonJarZips = locations.filterNot(Jar.isJarOrZip(_))
200-
if (nonJarZips.nonEmpty) {
201-
analyzer.macroLogVerbose(s"macro classloader: caching is disabled because the following paths are not supported: ${nonJarZips.mkString(",")}.")
202-
perRunCaches.recordClassloader(newLoader())
203-
} else {
204-
Macros.macroClassLoadersCache.getOrCreate(locations.map(_.jfile.toPath()), newLoader, closeableRegistry)
205-
}
206-
}
190+
val policy = settings.YcacheMacroClassLoader.value
191+
val cache = Macros.macroClassLoadersCache
192+
val disableCache = policy == settings.CachePolicy.None.name
193+
val checkStamps = policy == settings.CachePolicy.LastModified.name
194+
cache.checkCacheability(classpath, checkStamps, disableCache) match {
195+
case Left(msg) =>
196+
analyzer.macroLogVerbose(s"macro classloader: $msg.")
197+
val loader = newLoader()
198+
closeableRegistry.registerClosable(loader)
199+
loader
200+
case Right(paths) =>
201+
cache.getOrCreate(paths, newLoader, closeableRegistry, checkStamps)
207202
}
208203
}
209204
}

src/compiler/scala/tools/nsc/settings/ScalaSettings.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,10 @@ trait ScalaSettings extends AbsScalaSettings
277277
def setting(style: String, styleLong: String) = ChoiceSetting(s"-Ycache-$style-class-loader", "policy", s"Policy for caching class loaders for $styleLong that are dynamically loaded.", values.map(_.name), None.name, values.map(_.help))
278278
object None extends CachePolicy("none", "Don't cache class loader")
279279
object LastModified extends CachePolicy("last-modified", "Cache class loader, using file last-modified time to invalidate")
280+
object Always extends CachePolicy("always", "Cache class loader with no invalidation")
280281
// TODO Jorge to add new policy. Think about whether there is a benefit to the user on offering this as a separate policy or unifying with the previous one.
281282
// object ZipMetadata extends CachePolicy("zip-metadata", "Cache classloade, using file last-modified time, then ZIP file metadata to invalidate")
282-
def values: List[CachePolicy] = List(None, LastModified)
283+
def values: List[CachePolicy] = List(None, LastModified, Always)
283284
}
284285

285286
object optChoices extends MultiChoiceEnumeration {

0 commit comments

Comments
 (0)