Skip to content

Commit 30c372a

Browse files
committed
Make Global closeable
Close Global-scoped JARs and ClassLoaders. Entries of FileBasedCache that have a lifetime that exceeds any single Global instance are reference counted. When the count hits zero, it is closed if no references appear after a delay.
1 parent 837f924 commit 30c372a

28 files changed

+272
-113
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Scala (https://www.scala-lang.org)
3+
*
4+
* Copyright EPFL and Lightbend, Inc.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (http://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package scala.tools.nsc
14+
15+
import scala.util.control.NonFatal
16+
17+
/** Registry for resources to close when `Global` is closed */
18+
final class CloseableRegistry {
19+
private[this] var closeables: List[java.io.Closeable] = Nil
20+
final def registerClosable(c: java.io.Closeable): Unit = {
21+
closeables ::= c
22+
}
23+
24+
def close(): Unit = {
25+
for (c <- closeables) {
26+
try {
27+
c.close()
28+
} catch {
29+
case NonFatal(_) =>
30+
}
31+
}
32+
closeables = Nil
33+
}
34+
}

src/compiler/scala/tools/nsc/GenericRunnerSettings.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@ import java.net.URL
1616
import scala.tools.util.PathResolver
1717

1818
class GenericRunnerSettings(error: String => Unit) extends Settings(error) {
19-
lazy val classpathURLs: Seq[URL] = new PathResolver(this).resultAsURLs
19+
lazy val classpathURLs: Seq[URL] = {
20+
val registry = new CloseableRegistry
21+
try {
22+
new PathResolver(this, new CloseableRegistry).resultAsURLs
23+
} finally {
24+
registry.close()
25+
}
26+
}
2027

2128
val howtorun =
2229
ChoiceSetting(

src/compiler/scala/tools/nsc/Global.scala

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@ import scala.language.postfixOps
4040
import scala.tools.nsc.ast.{TreeGen => AstTreeGen}
4141
import scala.tools.nsc.classpath._
4242
import scala.tools.nsc.profile.Profiler
43+
import scala.util.control.NonFatal
44+
import java.io.Closeable
4345

4446
class Global(var currentSettings: Settings, reporter0: LegacyReporter)
4547
extends SymbolTable
48+
with Closeable
4649
with CompilationUnits
4750
with Plugins
4851
with PhaseAssembly
@@ -814,7 +817,7 @@ class Global(var currentSettings: Settings, reporter0: LegacyReporter)
814817

815818
/** Extend classpath of `platform` and rescan updated packages. */
816819
def extendCompilerClassPath(urls: URL*): Unit = {
817-
val urlClasspaths = urls.map(u => ClassPathFactory.newClassPath(AbstractFile.getURL(u), settings))
820+
val urlClasspaths = urls.map(u => ClassPathFactory.newClassPath(AbstractFile.getURL(u), settings, closeableRegistry))
818821
val newClassPath = AggregateClassPath.createAggregate(platform.classPath +: urlClasspaths : _*)
819822
platform.currentClassPath = Some(newClassPath)
820823
invalidateClassPathEntries(urls.map(_.getPath): _*)
@@ -876,7 +879,7 @@ class Global(var currentSettings: Settings, reporter0: LegacyReporter)
876879
}
877880
entries(classPath) find matchesCanonical match {
878881
case Some(oldEntry) =>
879-
Some(oldEntry -> ClassPathFactory.newClassPath(dir, settings))
882+
Some(oldEntry -> ClassPathFactory.newClassPath(dir, settings, closeableRegistry))
880883
case None =>
881884
error(s"Error adding entry to classpath. During invalidation, no entry named $path in classpath $classPath")
882885
None
@@ -1685,6 +1688,13 @@ class Global(var currentSettings: Settings, reporter0: LegacyReporter)
16851688
}
16861689

16871690
def createJavadoc = false
1691+
1692+
final val closeableRegistry: CloseableRegistry = new CloseableRegistry
1693+
1694+
def close(): Unit = {
1695+
perRunCaches.clearAll()
1696+
closeableRegistry.close()
1697+
}
16881698
}
16891699

16901700
object Global {

src/compiler/scala/tools/nsc/backend/JavaPlatform.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ trait JavaPlatform extends Platform {
2727
private[nsc] var currentClassPath: Option[ClassPath] = None
2828

2929
private[nsc] def classPath: ClassPath = {
30-
if (currentClassPath.isEmpty) currentClassPath = Some(new PathResolver(settings).result)
30+
if (currentClassPath.isEmpty) currentClassPath = Some(new PathResolver(settings, global.closeableRegistry).result)
3131
currentClassPath.get
3232
}
3333

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
6565
override def asClassPathStrings: Seq[String] = aggregates.map(_.asClassPathString).distinct
6666

6767
override def asSourcePathString: String = ClassPath.join(aggregates map (_.asSourcePathString): _*)
68-
6968
override private[nsc] def packages(inPackage: String): Seq[PackageEntry] = {
7069
val aggregatedPackages = aggregates.flatMap(_.packages(inPackage)).distinct
7170
aggregatedPackages

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@ package scala.tools.nsc.classpath
1414

1515
import scala.reflect.io.{AbstractFile, VirtualDirectory}
1616
import scala.reflect.io.Path.string2path
17-
import scala.tools.nsc.Settings
17+
import scala.tools.nsc.{CloseableRegistry, Settings}
1818
import FileUtils.AbstractFileOps
1919
import scala.tools.nsc.util.ClassPath
2020

2121
/**
2222
* Provides factory methods for classpath. When creating classpath instances for a given path,
2323
* it uses proper type of classpath depending on a types of particular files containing sources or classes.
2424
*/
25-
class ClassPathFactory(settings: Settings) {
25+
class ClassPathFactory(settings: Settings, closeableRegistry: CloseableRegistry) {
2626
/**
2727
* Create a new classpath based on the abstract file.
2828
*/
29-
def newClassPath(file: AbstractFile): ClassPath = ClassPathFactory.newClassPath(file, settings)
29+
def newClassPath(file: AbstractFile): ClassPath = ClassPathFactory.newClassPath(file, settings, closeableRegistry)
3030

3131
/**
3232
* Creators for sub classpaths which preserve this context.
@@ -70,19 +70,19 @@ class ClassPathFactory(settings: Settings) {
7070

7171
private def createSourcePath(file: AbstractFile): ClassPath =
7272
if (file.isJarOrZip)
73-
ZipAndJarSourcePathFactory.create(file, settings)
73+
ZipAndJarSourcePathFactory.create(file, settings, closeableRegistry)
7474
else if (file.isDirectory)
7575
DirectorySourcePath(file.file)
7676
else
7777
throw new IllegalArgumentException(s"Unsupported sourcepath element: $file")
7878
}
7979

8080
object ClassPathFactory {
81-
def newClassPath(file: AbstractFile, settings: Settings): ClassPath = file match {
81+
def newClassPath(file: AbstractFile, settings: Settings, closeableRegistry: CloseableRegistry): ClassPath = file match {
8282
case vd: VirtualDirectory => VirtualDirectoryClassPath(vd)
8383
case _ =>
8484
if (file.isJarOrZip)
85-
ZipAndJarClassPathFactory.create(file, settings)
85+
ZipAndJarClassPathFactory.create(file, settings, closeableRegistry)
8686
else if (file.isDirectory)
8787
DirectoryClassPath(file.file)
8888
else

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
package scala.tools.nsc.classpath
1414

15-
import java.io.File
15+
import java.io.{Closeable, File}
1616
import java.net.URL
1717
import java.nio.file.{FileSystems, Files}
1818
import java.util
@@ -22,6 +22,7 @@ import scala.tools.nsc.util.{ClassPath, ClassRepresentation}
2222
import FileUtils._
2323
import scala.collection.JavaConverters._
2424
import scala.reflect.internal.JDK9Reflectors
25+
import scala.tools.nsc.CloseableRegistry
2526
import scala.tools.nsc.classpath.PackageNameUtils.{packageContains, separatePkgAndClassNames}
2627

2728
/**
@@ -58,6 +59,7 @@ trait DirectoryLookup[FileEntryType <: ClassRepresentation] extends ClassPath {
5859

5960
private[nsc] def packages(inPackage: String): Seq[PackageEntry] = {
6061
val dirForPackage = getDirectory(inPackage)
62+
6163
val nestedDirs: Array[F] = dirForPackage match {
6264
case None => emptyFiles
6365
case Some(directory) => listChildren(directory, Some(isPackage))
@@ -134,7 +136,7 @@ trait JFileDirectoryLookup[FileEntryType <: ClassRepresentation] extends Directo
134136

135137
object JrtClassPath {
136138
import java.nio.file._, java.net.URI
137-
def apply(release: Option[String]): Option[ClassPath] = {
139+
def apply(release: Option[String], closeableRegistry: CloseableRegistry): Option[ClassPath] = {
138140
import scala.util.Properties._
139141
if (!isJavaAtLeast("9")) None
140142
else {
@@ -151,7 +153,11 @@ object JrtClassPath {
151153
try {
152154
val ctSym = Paths.get(javaHome).resolve("lib").resolve("ct.sym")
153155
if (Files.notExists(ctSym)) None
154-
else Some(new CtSymClassPath(ctSym, v.toInt))
156+
else {
157+
val classPath = new CtSymClassPath(ctSym, v.toInt)
158+
closeableRegistry.registerClosable(classPath)
159+
Some(classPath)
160+
}
155161
} catch {
156162
case _: Throwable => None
157163
}
@@ -227,7 +233,7 @@ final class JrtClassPath(fs: java.nio.file.FileSystem) extends ClassPath with No
227233
/**
228234
* Implementation `ClassPath` based on the $JAVA_HOME/lib/ct.sym backing http://openjdk.java.net/jeps/247
229235
*/
230-
final class CtSymClassPath(ctSym: java.nio.file.Path, release: Int) extends ClassPath with NoSourcePaths {
236+
final class CtSymClassPath(ctSym: java.nio.file.Path, release: Int) extends ClassPath with NoSourcePaths with Closeable {
231237
import java.nio.file.Path, java.nio.file._
232238

233239
private val fileSystem: FileSystem = FileSystems.newFileSystem(ctSym, null)
@@ -273,7 +279,7 @@ final class CtSymClassPath(ctSym: java.nio.file.Path, release: Int) extends Clas
273279

274280
def asURLs: Seq[URL] = Nil
275281
def asClassPathStrings: Seq[String] = Nil
276-
282+
override def close(): Unit = fileSystem.close()
277283
def findClassFile(className: String): Option[AbstractFile] = {
278284
if (!className.contains(".")) None
279285
else {

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ case class VirtualDirectoryClassPath(dir: VirtualDirectory) extends ClassPath wi
3737
// mimic the behavior of the old nsc.util.DirectoryClassPath
3838
def asURLs: Seq[URL] = Seq(new URL(dir.name))
3939
def asClassPathStrings: Seq[String] = Seq(dir.path)
40-
4140
override def findClass(className: String): Option[ClassRepresentation] = findClassFile(className) map ClassFileEntryImpl
4241

4342
def findClassFile(className: String): Option[AbstractFile] = {

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

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@
1212

1313
package scala.tools.nsc.classpath
1414

15-
import java.io.File
15+
import java.io.{Closeable, File}
1616
import java.net.URL
1717
import java.nio.file.Files
1818
import java.nio.file.attribute.{BasicFileAttributes, FileTime}
19+
import java.util.{Timer, TimerTask}
20+
import java.util.concurrent.atomic.AtomicInteger
1921

2022
import scala.annotation.tailrec
2123
import scala.reflect.io.{AbstractFile, FileZipArchive, ManifestResources}
2224
import scala.tools.nsc.util.{ClassPath, ClassRepresentation}
23-
import scala.tools.nsc.Settings
25+
import scala.tools.nsc.{CloseableRegistry, Settings}
2426
import FileUtils._
2527

2628
/**
@@ -29,18 +31,19 @@ import FileUtils._
2931
* when there are a lot of projects having a lot of common dependencies.
3032
*/
3133
sealed trait ZipAndJarFileLookupFactory {
32-
private val cache = new FileBasedCache[ClassPath]
33-
34-
def create(zipFile: AbstractFile, settings: Settings): ClassPath = {
35-
if (settings.YdisableFlatCpCaching || zipFile.file == null) createForZipFile(zipFile, settings.releaseValue)
36-
else createUsingCache(zipFile, settings)
34+
private val cache = new FileBasedCache[ClassPath with Closeable]
35+
36+
def create(zipFile: AbstractFile, settings: Settings, closeableRegistry: CloseableRegistry): ClassPath = {
37+
if (settings.YdisableFlatCpCaching || zipFile.file == null) {
38+
val result: ClassPath with Closeable = createForZipFile(zipFile, settings.releaseValue)
39+
closeableRegistry.registerClosable(result)
40+
result
41+
} else {
42+
cache.getOrCreate(List(zipFile.file.toPath), () => createForZipFile(zipFile, settings.releaseValue), closeableRegistry)
43+
}
3744
}
3845

39-
protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath
40-
41-
private def createUsingCache(zipFile: AbstractFile, settings: Settings): ClassPath = {
42-
cache.getOrCreate(List(zipFile.file.toPath), () => createForZipFile(zipFile, settings.releaseValue))
43-
}
46+
protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath with Closeable
4447
}
4548

4649
/**
@@ -75,7 +78,7 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
7578
* with a particularly prepared scala-library.jar. It should have all classes listed in the manifest like e.g. this entry:
7679
* Name: scala/Function2$mcFJD$sp.class
7780
*/
78-
private case class ManifestResourcesClassPath(file: ManifestResources) extends ClassPath with NoSourcePaths {
81+
private case class ManifestResourcesClassPath(file: ManifestResources) extends ClassPath with NoSourcePaths with Closeable {
7982
override def findClassFile(className: String): Option[AbstractFile] = {
8083
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
8184
classes(pkg).find(_.name == simpleClassName).map(_.file)
@@ -84,6 +87,7 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
8487
override def asClassPathStrings: Seq[String] = Seq(file.path)
8588

8689
override def asURLs: Seq[URL] = file.toURLs()
90+
override def close(): Unit = file.close()
8791

8892
import ManifestResourcesClassPath.PackageFileInfo
8993
import ManifestResourcesClassPath.PackageInfo
@@ -152,7 +156,7 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
152156
case class PackageInfo(packageName: String, subpackages: List[AbstractFile])
153157
}
154158

155-
override protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath =
159+
override protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath with Closeable =
156160
if (zipFile.file == null) createWithoutUnderlyingFile(zipFile)
157161
else ZipArchiveClassPath(zipFile.file, release)
158162

@@ -183,15 +187,33 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
183187
override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isScalaOrJavaSource
184188
}
185189

186-
override protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath = ZipArchiveSourcePath(zipFile.file)
190+
override protected def createForZipFile(zipFile: AbstractFile, release: Option[String]): ClassPath with Closeable = ZipArchiveSourcePath(zipFile.file)
187191
}
188192

189193
final class FileBasedCache[T] {
190194
import java.nio.file.Path
191195
private case class Stamp(lastModified: FileTime, fileKey: Object)
192-
private val cache = collection.mutable.Map.empty[Seq[Path], (Seq[Stamp], T)]
196+
private case class Entry(stamps: Seq[Stamp], t: T) {
197+
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+
}
209+
}
210+
}
211+
}
212+
}
213+
}
214+
private val cache = collection.mutable.Map.empty[Seq[Path], Entry]
193215

194-
def getOrCreate(paths: Seq[Path], create: () => T): T = cache.synchronized {
216+
def getOrCreate(paths: Seq[Path], create: () => T, closeableRegistry: CloseableRegistry): T = cache.synchronized {
195217
val stamps = paths.map { path =>
196218
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
197219
val lastModified = attrs.lastModifiedTime()
@@ -201,10 +223,15 @@ final class FileBasedCache[T] {
201223
}
202224

203225
cache.get(paths) match {
204-
case Some((cachedStamps, cached)) if cachedStamps == stamps => cached
226+
case Some(e@Entry(cachedStamps, cached)) if cachedStamps == stamps =>
227+
e.referenceCount.incrementAndGet()
228+
closeableRegistry.registerClosable(e.referenceCountDecrementer)
229+
cached
205230
case _ =>
206231
val value = create()
207-
cache.put(paths, (stamps, value))
232+
val entry = Entry(stamps, value)
233+
cache.put(paths, entry)
234+
closeableRegistry.registerClosable(entry.referenceCountDecrementer)
208235
value
209236
}
210237
}
@@ -215,3 +242,26 @@ final class FileBasedCache[T] {
215242
cache.clear()
216243
}
217244
}
245+
246+
object FileBasedCache {
247+
private val deferCloseMs = Integer.getInteger("scalac.filebasedcache.defer.close.ms", 1000)
248+
private val timer: Option[Timer] = {
249+
if (deferCloseMs > 0)
250+
Some(new java.util.Timer(true))
251+
else None
252+
}
253+
private def deferredClose(referenceCount: AtomicInteger, closable: Closeable): Unit = {
254+
timer match {
255+
case Some(timer) =>
256+
val task = new TimerTask {
257+
override def run(): Unit = {
258+
if (referenceCount.get == 0)
259+
closable.close()
260+
}
261+
}
262+
timer.schedule(task, FileBasedCache.deferCloseMs.toLong)
263+
case None =>
264+
closable.close()
265+
}
266+
}
267+
}

0 commit comments

Comments
 (0)