Skip to content

Commit 51a317c

Browse files
authored
Merge pull request #3116 from dotty-staging/classpath
Port ClassPath optimisations and bug fixes from scalac
2 parents 660fab6 + 17246c0 commit 51a317c

File tree

8 files changed

+188
-51
lines changed

8 files changed

+188
-51
lines changed

compiler/src/dotty/tools/dotc/classpath/AggregateClassPath.scala

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,28 @@ import dotty.tools.io.{ AbstractFile, ClassPath, ClassRepresentation }
1919
*/
2020
case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
2121
override def findClassFile(className: String): Option[AbstractFile] = {
22-
@tailrec
23-
def find(aggregates: Seq[ClassPath]): Option[AbstractFile] =
24-
if (aggregates.nonEmpty) {
25-
val classFile = aggregates.head.findClassFile(className)
26-
if (classFile.isDefined) classFile
27-
else find(aggregates.tail)
28-
} else None
29-
30-
find(aggregates)
22+
val (pkg, _) = PackageNameUtils.separatePkgAndClassNames(className)
23+
aggregatesForPackage(pkg).iterator.map(_.findClassFile(className)).collectFirst {
24+
case Some(x) => x
25+
}
26+
}
27+
private[this] val packageIndex: collection.mutable.Map[String, Seq[ClassPath]] = collection.mutable.Map()
28+
private def aggregatesForPackage(pkg: String): Seq[ClassPath] = packageIndex.synchronized {
29+
packageIndex.getOrElseUpdate(pkg, aggregates.filter(_.hasPackage(pkg)))
3130
}
3231

3332
override def findClass(className: String): Option[ClassRepresentation] = {
34-
@tailrec
35-
def findEntry(aggregates: Seq[ClassPath], isSource: Boolean): Option[ClassRepresentation] =
36-
if (aggregates.nonEmpty) {
37-
val entry = aggregates.head.findClass(className) match {
38-
case s @ Some(_: SourceFileEntry) if isSource => s
39-
case s @ Some(_: ClassFileEntry) if !isSource => s
40-
case _ => None
41-
}
42-
if (entry.isDefined) entry
43-
else findEntry(aggregates.tail, isSource)
44-
} else None
45-
46-
val classEntry = findEntry(aggregates, isSource = false)
47-
val sourceEntry = findEntry(aggregates, isSource = true)
33+
val (pkg, _) = PackageNameUtils.separatePkgAndClassNames(className)
34+
35+
def findEntry(isSource: Boolean): Option[ClassRepresentation] = {
36+
aggregatesForPackage(pkg).iterator.map(_.findClass(className)).collectFirst {
37+
case Some(s: SourceFileEntry) if isSource => s
38+
case Some(s: ClassFileEntry) if !isSource => s
39+
}
40+
}
41+
42+
val classEntry = findEntry(isSource = false)
43+
val sourceEntry = findEntry(isSource = true)
4844

4945
(classEntry, sourceEntry) match {
5046
case (Some(c: ClassFileEntry), Some(s: SourceFileEntry)) => Some(ClassAndSourceFilesEntry(c.file, s.file))
@@ -70,6 +66,7 @@ case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
7066
override private[dotty] def sources(inPackage: String): Seq[SourceFileEntry] =
7167
getDistinctEntries(_.sources(inPackage))
7268

69+
override private[dotty] def hasPackage(pkg: String) = aggregates.exists(_.hasPackage(pkg))
7370
override private[dotty] def list(inPackage: String): ClassPathEntries = {
7471
val (packages, classesAndSources) = aggregates.map { cp =>
7572
try {

compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ trait DirectoryLookup[FileEntryType <: ClassRepresentation] extends ClassPath {
4545
}
4646
}
4747

48+
override private[dotty] def hasPackage(pkg: String) = getDirectory(pkg).isDefined
49+
4850
private[dotty] def packages(inPackage: String): Seq[PackageEntry] = {
4951
val dirForPackage = getDirectory(inPackage)
5052
val nestedDirs: Array[F] = dirForPackage match {
@@ -161,6 +163,9 @@ final class JrtClassPath(fs: java.nio.file.FileSystem) extends ClassPath with No
161163
ps.map(p => (p.toString.stripPrefix("/packages/"), lookup(p))).toMap
162164
}
163165

166+
/** Empty string represents root package */
167+
override private[dotty] def hasPackage(pkg: String) = packageToModuleBases.contains(pkg)
168+
164169
override private[dotty] def packages(inPackage: String): Seq[PackageEntry] = {
165170
def matches(packageDottedName: String) =
166171
if (packageDottedName.contains("."))

compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,31 @@ package dotty.tools.dotc.classpath
55

66
import java.io.File
77
import java.net.URL
8+
import java.nio.file.Files
9+
import java.nio.file.attribute.{BasicFileAttributes, FileTime}
10+
811
import scala.annotation.tailrec
9-
import dotty.tools.io.{AbstractFile, ClassPath, FileZipArchive, ManifestResources}
10-
import dotty.tools.dotc.config.Settings
12+
import dotty.tools.io.{AbstractFile, ClassPath, ClassRepresentation, FileZipArchive, ManifestResources}
1113
import dotty.tools.dotc.core.Contexts.Context
1214
import FileUtils._
1315

1416
/**
1517
* A trait providing an optional cache for classpath entries obtained from zip and jar files.
16-
* It's possible to create such a cache assuming that entries in such files won't change (at
17-
* least will be the same each time we'll load classpath during the lifetime of JVM process)
18-
* - unlike class and source files in directories, which can be modified and recompiled.
1918
* It allows us to e.g. reduce significantly memory used by PresentationCompilers in Scala IDE
2019
* when there are a lot of projects having a lot of common dependencies.
2120
*/
2221
sealed trait ZipAndJarFileLookupFactory {
23-
private val cache = collection.mutable.Map.empty[AbstractFile, ClassPath]
22+
private val cache = new FileBasedCache[ClassPath]
2423

2524
def create(zipFile: AbstractFile)(implicit ctx: Context): ClassPath = {
26-
if (ctx.settings.YdisableFlatCpCaching.value) createForZipFile(zipFile)
25+
if (ctx.settings.YdisableFlatCpCaching.value || zipFile.file == null) createForZipFile(zipFile)
2726
else createUsingCache(zipFile)
2827
}
2928

3029
protected def createForZipFile(zipFile: AbstractFile): ClassPath
3130

32-
private def createUsingCache(zipFile: AbstractFile)(implicit ctx: Context): ClassPath = cache.synchronized {
33-
def newClassPathInstance = {
34-
if (ctx.settings.verbose.value || ctx.settings.Ylogcp.value)
35-
println(s"$zipFile is not yet in the classpath cache")
36-
createForZipFile(zipFile)
37-
}
38-
cache.getOrElseUpdate(zipFile, newClassPathInstance)
39-
}
31+
private def createUsingCache(zipFile: AbstractFile): ClassPath =
32+
cache.getOrCreate(zipFile.file.toPath, () => createForZipFile(zipFile))
4033
}
4134

4235
/**
@@ -50,7 +43,13 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
5043

5144
override def findClassFile(className: String): Option[AbstractFile] = {
5245
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
53-
classes(pkg).find(_.name == simpleClassName).map(_.file)
46+
file(pkg, simpleClassName + ".class").map(_.file)
47+
}
48+
49+
// This method is performance sensitive as it is used by SBT's ExtractDependencies phase.
50+
override def findClass(className: String): Option[ClassRepresentation] = {
51+
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
52+
file(pkg, simpleClassName + ".class")
5453
}
5554

5655
override private[dotty] def classes(inPackage: String): Seq[ClassFileEntry] = files(inPackage)
@@ -133,6 +132,7 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
133132
(for (file <- pkg if file.isClass) yield ClassFileEntryImpl(file))(collection.breakOut)
134133
}
135134

135+
override private[dotty] def hasPackage(pkg: String) = cachedPackages.contains(pkg)
136136
override private[dotty] def list(inPackage: String): ClassPathEntries = ClassPathEntries(packages(inPackage), classes(inPackage))
137137
}
138138

@@ -173,3 +173,29 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
173173

174174
override protected def createForZipFile(zipFile: AbstractFile): ClassPath = ZipArchiveSourcePath(zipFile.file)
175175
}
176+
177+
final class FileBasedCache[T] {
178+
private case class Stamp(lastModified: FileTime, fileKey: Object)
179+
private val cache = collection.mutable.Map.empty[java.nio.file.Path, (Stamp, T)]
180+
181+
def getOrCreate(path: java.nio.file.Path, create: () => T): T = cache.synchronized {
182+
val attrs = Files.readAttributes(path, classOf[BasicFileAttributes])
183+
val lastModified = attrs.lastModifiedTime()
184+
// only null on some platforms, but that's okay, we just use the last modified timestamp as our stamp
185+
val fileKey = attrs.fileKey()
186+
val stamp = Stamp(lastModified, fileKey)
187+
cache.get(path) match {
188+
case Some((cachedStamp, cached)) if cachedStamp == stamp => cached
189+
case _ =>
190+
val value = create()
191+
cache.put(path, (stamp, value))
192+
value
193+
}
194+
}
195+
196+
def clear(): Unit = cache.synchronized {
197+
// TODO support closing
198+
// cache.valuesIterator.foreach(_.close())
199+
cache.clear()
200+
}
201+
}

compiler/src/dotty/tools/dotc/classpath/ZipArchiveFileLookup.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ trait ZipArchiveFileLookup[FileEntryType <: ClassRepresentation] extends ClassPa
3939
entry <- dirEntry.iterator if isRequiredFileType(entry)
4040
} yield createFileEntry(entry)
4141

42+
protected def file(inPackage: String, name: String): Option[FileEntryType] =
43+
for {
44+
dirEntry <- findDirEntry(inPackage)
45+
entry <- Option(dirEntry.lookupName(name, directory = false))
46+
if isRequiredFileType(entry)
47+
} yield createFileEntry(entry)
48+
49+
override private[dotty] def hasPackage(pkg: String) = findDirEntry(pkg).isDefined
4250
override private[dotty] def list(inPackage: String): ClassPathEntries = {
4351
val foundDirEntry = findDirEntry(inPackage)
4452

compiler/src/dotty/tools/io/ClassPath.scala

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,42 @@ trait ClassPath {
2121
import dotty.tools.dotc.classpath._
2222
def asURLs: Seq[URL]
2323

24-
/** Empty string represents root package */
24+
/*
25+
* These methods are mostly used in the ClassPath implementation to implement the `list` and
26+
* `findX` methods below.
27+
*
28+
* However, there are some other uses in the compiler, to implement `invalidateClassPathEntries`,
29+
* which is used by the repl's `:require` (and maybe the spark repl, https://github.com/scala/scala/pull/4051).
30+
* Using these methods directly is more efficient than calling `list`.
31+
*
32+
* The `inPackage` string is a full package name, e.g. "" or "scala.collection".
33+
*/
34+
35+
private[dotty] def hasPackage(pkg: String): Boolean
2536
private[dotty] def packages(inPackage: String): Seq[PackageEntry]
2637
private[dotty] def classes(inPackage: String): Seq[ClassFileEntry]
2738
private[dotty] def sources(inPackage: String): Seq[SourceFileEntry]
2839

29-
/** Allows to get entries for packages and classes merged with sources possibly in one pass. */
40+
/**
41+
* Returns packages and classes (source or classfile) that are members of `inPackage` (not
42+
* recursively). The `inPackage` string is a full package name, e.g., "scala.collection".
43+
*
44+
* This is the main method uses to find classes, see class `PackageLoader`. The
45+
* `rootMirror.rootLoader` is created with `inPackage = ""`.
46+
*/
3047
private[dotty] def list(inPackage: String): ClassPathEntries
3148

3249
/**
33-
* It returns both classes from class file and source files (as our base ClassRepresentation).
34-
* So note that it's not so strictly related to findClassFile.
50+
* Returns the class file and / or source file for a given external name, e.g., "java.lang.String".
51+
* If there is both a class file and source file, the compiler can decide whether to read the
52+
* class file or compile the source file.
53+
*
54+
* Internally this seems to be used only by `ScriptRunner`, but only to call `.isDefined`. That
55+
* could probably be implemented differently.
56+
*
57+
* Externally, it is used by sbt's compiler interface:
58+
* https://github.com/sbt/sbt/blob/v0.13.15/compile/interface/src/main/scala/xsbt/CompilerInterface.scala#L249
59+
* Jason has some improvements for that in the works (https://github.com/scala/bug/issues/10289#issuecomment-310022699)
3560
*/
3661
def findClass(className: String): Option[ClassRepresentation] = {
3762
// A default implementation which should be overridden, if we can create the more efficient
@@ -43,6 +68,16 @@ trait ClassPath {
4368

4469
foundClassFromClassFiles orElse findClassInSources
4570
}
71+
72+
/**
73+
* Returns the classfile for an external name, e.g., "java.lang.String". This method does not
74+
* return source files.
75+
*
76+
* This method is used by the classfile parser. When parsing a Java class, its own inner classes
77+
* are entered with a `ClassfileLoader` that parses the classfile returned by this method.
78+
* It is also used in the backend, by the inliner, to obtain the bytecode when inlining from the
79+
* classpath. It's also used by scalap.
80+
*/
4681
def findClassFile(className: String): Option[AbstractFile]
4782

4883
def asClassPathStrings: Seq[String]

compiler/src/dotty/tools/io/ZipArchive.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import scala.annotation.tailrec
2424
* ''Note: This library is considered experimental and should not be used unless you know what you are doing.''
2525
*/
2626
object ZipArchive {
27-
private[io] val closeZipFile = sys.props.get("scala.classpath.closeZip").map(_.toBoolean).getOrElse(false)
27+
private[io] val closeZipFile = sys.props.get("scala.classpath.closeZip").exists(_.toBoolean)
2828

2929
/**
3030
* @param file a File
@@ -82,8 +82,8 @@ abstract class ZipArchive(override val file: JFile) extends AbstractFile with Eq
8282
override def isDirectory = true
8383
override def iterator: Iterator[Entry] = entries.valuesIterator
8484
override def lookupName(name: String, directory: Boolean): Entry = {
85-
if (directory) entries(name + "/")
86-
else entries(name)
85+
if (directory) entries.get(name + "/").orNull
86+
else entries.get(name).orNull
8787
}
8888
}
8989

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package dotty.tools.dotc.classpath
2+
3+
import org.junit.Test
4+
import java.nio.file._
5+
import java.nio.file.attribute.FileTime
6+
7+
import dotty.tools.dotc.core.Contexts.{Context, ContextBase}
8+
import dotty.tools.io.AbstractFile
9+
10+
11+
12+
class ZipAndJarFileLookupFactoryTest {
13+
@Test def cacheInvalidation(): Unit = {
14+
if (scala.util.Properties.isWin) return // can't overwrite an open file on windows.
15+
16+
val f = Files.createTempFile("test-", ".jar")
17+
Files.delete(f)
18+
19+
implicit val ctx: Context = new ContextBase().initialCtx
20+
assert(!ctx.settings.YdisableFlatCpCaching.value) // we're testing with our JAR metadata caching enabled.
21+
22+
def createCp = ZipAndJarClassPathFactory.create(AbstractFile.getFile(f.toFile))
23+
try {
24+
createZip(f, Array(), "p1/C.class")
25+
createZip(f, Array(), "p2/X.class")
26+
createZip(f, Array(), "p3/Y.class")
27+
val cp1 = createCp
28+
assert(cp1.findClass("p1.C").isDefined)
29+
30+
// We expect get a cache hit as the underlying zip hasn't changed
31+
val cp2 = createCp
32+
assert(cp2 eq cp1)
33+
34+
// check things work after the cache hit
35+
cp1.findClassFile("p2.X").get.toByteArray
36+
37+
val lastMod1 = Files.getLastModifiedTime(f)
38+
// Create a new zip at the same path with different contents and last modified
39+
Files.delete(f)
40+
createZip(f, Array(), "p1/D.class")
41+
Files.setLastModifiedTime(f, FileTime.fromMillis(lastMod1.toMillis + 2000))
42+
43+
// Our classpath cache should create a new instance
44+
val cp3 = createCp
45+
assert(cp1 ne cp3, (System.identityHashCode(cp1), System.identityHashCode(cp3)))
46+
// And that instance should see D, not C, in package p1.
47+
assert(cp3.findClass("p1.C").isEmpty)
48+
assert(cp3.findClass("p1.D").isDefined)
49+
} finally Files.delete(f)
50+
}
51+
52+
def createZip(zipLocation: Path, content: Array[Byte], internalPath: String): Unit = {
53+
val env = new java.util.HashMap[String, String]()
54+
env.put("create", String.valueOf(Files.notExists(zipLocation)))
55+
val fileUri = zipLocation.toUri
56+
val zipUri = new java.net.URI("jar:" + fileUri.getScheme, fileUri.getPath, null)
57+
val zipfs = FileSystems.newFileSystem(zipUri, env)
58+
try {
59+
try {
60+
val internalTargetPath = zipfs.getPath(internalPath)
61+
Files.createDirectories(internalTargetPath.getParent)
62+
Files.write(internalTargetPath, content)
63+
} finally {
64+
if (zipfs != null) zipfs.close()
65+
}
66+
} finally {
67+
zipfs.close()
68+
}
69+
}
70+
}

sbt-dotty/sbt-test/source-dependencies/binary/project/P.scala

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ object B extends Build
55
{
66
lazy val dep = Project("dep", file("dep"))
77
lazy val use = Project("use", file("use")) settings(
8-
unmanagedJars in Compile <+= packageBin in (dep, Compile) map Attributed.blank,
9-
10-
// Disable classpath caching since it does not invalidate modified jars
11-
// (https://github.com/scala/bug/issues/10295)
12-
scalacOptions += "-YdisableFlatCpCaching"
8+
unmanagedJars in Compile <+= packageBin in (dep, Compile) map Attributed.blank
139
)
1410
}

0 commit comments

Comments
 (0)