Skip to content

Commit 09c7edc

Browse files
committed
Faster and simpler Java 9 classpath implementation
- Take advantage of the `/packages` index provided by the jrt file system to avoid (expensive) Files.exist for non-existent entries across the full list of modules. - Extends ClassPath directly which leads to a simpler implemnentation that using the base class. - Add a unit test that shows we can read classes and packages from the Java standard library. Fixes scala/scala-dev#306 With this change bootstrap time under Java 9 was comparable to Java 8. Before, it was about 40% slower.
1 parent 502e3c6 commit 09c7edc

File tree

3 files changed

+99
-30
lines changed

3 files changed

+99
-30
lines changed

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

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import java.util.function.IntFunction
1010
import java.util
1111
import java.util.Comparator
1212

13-
import scala.reflect.io.{AbstractFile, PlainFile}
13+
import scala.reflect.io.{AbstractFile, PlainFile, PlainNioFile}
1414
import scala.tools.nsc.util.{ClassPath, ClassRepresentation}
1515
import FileUtils._
16+
import scala.collection.JavaConverters._
1617

1718
/**
1819
* A trait allowing to look for classpath entries in directories. It provides common logic for
@@ -121,51 +122,78 @@ trait JFileDirectoryLookup[FileEntryType <: ClassRepresentation] extends Directo
121122
def asClassPathStrings: Seq[String] = Seq(dir.getPath)
122123
}
123124

124-
object JImageDirectoryLookup {
125-
import java.nio.file._, java.net.URI, scala.collection.JavaConverters._
126-
def apply(): List[ClassPath] = {
125+
object JrtClassPath {
126+
import java.nio.file._, java.net.URI
127+
def apply(): Option[ClassPath] = {
127128
try {
128129
val fs = FileSystems.getFileSystem(URI.create("jrt:/"))
129-
val dir: Path = fs.getPath("/modules")
130-
val modules = Files.list(dir).iterator().asScala.toList
131-
modules.map(m => new JImageDirectoryLookup(fs, m.getFileName.toString))
130+
Some(new JrtClassPath(fs))
132131
} catch {
133132
case _: ProviderNotFoundException | _: FileSystemNotFoundException =>
134-
Nil
133+
None
135134
}
136135
}
137136
}
138-
class JImageDirectoryLookup(fs: java.nio.file.FileSystem, module: String) extends DirectoryLookup[ClassFileEntryImpl] with NoSourcePaths {
137+
138+
/**
139+
* Implementation `ClassPath` based on the JDK 9 encapsulated runtime modules (JEP-220)
140+
*
141+
* https://bugs.openjdk.java.net/browse/JDK-8066492 is the most up to date reference
142+
* for the structure of the jrt:// filesystem.
143+
*
144+
* The implementation assumes that no classes exist in the empty package.
145+
*/
146+
final class JrtClassPath(fs: java.nio.file.FileSystem) extends ClassPath with NoSourcePaths {
139147
import java.nio.file.Path, java.nio.file._
140148
type F = Path
141-
val dir: Path = fs.getPath("/modules/" + module)
149+
private val dir: Path = fs.getPath("/packages")
142150

143-
protected def emptyFiles: Array[Path] = Array.empty
144-
protected def getSubDir(packageDirName: String): Option[Path] = {
145-
val packageDir = dir.resolve(packageDirName)
146-
if (Files.exists(packageDir) && Files.isDirectory(packageDir)) Some(packageDir)
147-
else None
151+
// e.g. "java.lang" -> Seq("/modules/java.base")
152+
private val packageToModuleBases: Map[String, Seq[Path]] = {
153+
val ps = Files.newDirectoryStream(dir).iterator().asScala
154+
def lookup(pack: Path): Seq[Path] = {
155+
Files.list(pack).iterator().asScala.map(l => if (Files.isSymbolicLink(l)) Files.readSymbolicLink(l) else l).toList
156+
}
157+
ps.map(p => (p.toString.stripPrefix("/packages/"), lookup(p))).toMap
148158
}
149-
protected def listChildren(dir: Path, filter: Option[Path => Boolean]): Array[Path] = {
150-
import scala.collection.JavaConverters._
151-
val f = filter.getOrElse((p: Path) => true)
152-
Files.list(dir).iterator().asScala.filter(f).toArray[Path]
159+
160+
override private[nsc] def packages(inPackage: String): Seq[PackageEntry] = {
161+
def matches(packageDottedName: String) =
162+
if (packageDottedName.contains("."))
163+
packageOf(packageDottedName) == inPackage
164+
else inPackage == ""
165+
packageToModuleBases.keysIterator.filter(matches).map(PackageEntryImpl(_)).toVector
166+
}
167+
private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = {
168+
if (inPackage == "") Nil
169+
else {
170+
packageToModuleBases.getOrElse(inPackage, Nil).flatMap(x =>
171+
Files.list(x.resolve(inPackage.replace('.', '/'))).iterator().asScala.filter(_.getFileName.toString.endsWith(".class"))).map(x =>
172+
ClassFileEntryImpl(new PlainNioFile(x))).toVector
173+
}
153174
}
154-
protected def getName(f: Path): String = f.getFileName.toString
155-
protected def toAbstractFile(f: Path): AbstractFile = new scala.reflect.io.PlainNioFile(f)
156-
protected def isPackage(f: Path): Boolean = Files.isDirectory(f) && mayBeValidPackage(f.getFileName.toString)
175+
176+
override private[nsc] def list(inPackage: String): ClassPathEntries =
177+
if (inPackage == "") ClassPathEntries(packages(inPackage), Nil)
178+
else ClassPathEntries(packages(inPackage), classes(inPackage))
157179

158180
def asURLs: Seq[URL] = Seq(dir.toUri.toURL)
159-
def asClassPathStrings: Seq[String] = asURLs.map(_.toString)
181+
// We don't yet have a scheme to represent the JDK modules in our `-classpath`.
182+
// java models them as entries in the new "module path", we'll probably need to follow this.
183+
def asClassPathStrings: Seq[String] = Nil
160184

161185
def findClassFile(className: String): Option[AbstractFile] = {
162-
val relativePath = FileUtils.dirPath(className) + ".class"
163-
val classFile = dir.resolve(relativePath)
164-
if (Files.exists(classFile)) Some(new scala.reflect.io.PlainNioFile(classFile)) else None
186+
if (!className.contains(".")) None
187+
else {
188+
val inPackage = packageOf(className)
189+
packageToModuleBases.getOrElse(inPackage, Nil).iterator.flatMap{x =>
190+
val file = x.resolve(className.replace('.', '/') + ".class")
191+
if (Files.exists(file)) new scala.reflect.io.PlainNioFile(file) :: Nil else Nil
192+
}.take(1).toList.headOption
193+
}
165194
}
166-
override protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
167-
override protected def isMatchingFile(f: Path): Boolean = Files.isRegularFile(f) && f.getFileName.toString.endsWith(".class")
168-
override private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = files(inPackage)
195+
private def packageOf(dottedClassName: String): String =
196+
dottedClassName.substring(0, dottedClassName.lastIndexOf("."))
169197
}
170198

171199
case class DirectoryClassPath(dir: File) extends JFileDirectoryLookup[ClassFileEntryImpl] with NoSourcePaths {

src/compiler/scala/tools/util/PathResolver.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ final class PathResolver(settings: Settings) {
234234

235235
// Assemble the elements!
236236
def basis = List[Traversable[ClassPath]](
237-
JImageDirectoryLookup.apply(), // 0. The Java 9 classpath (backed by the jrt:/ virtual system)
237+
JrtClassPath.apply(), // 0. The Java 9 classpath (backed by the jrt:/ virtual system, if available)
238238
classesInPath(javaBootClassPath), // 1. The Java bootstrap class path.
239239
contentsOfDirsInPath(javaExtDirs), // 2. The Java extension class path.
240240
classesInExpandedPath(javaUserClassPath), // 3. The Java application class path.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2014 Contributor. All rights reserved.
3+
*/
4+
package scala.tools.nsc.classpath
5+
6+
import org.junit.Assert._
7+
import org.junit.Test
8+
import org.junit.runner.RunWith
9+
import org.junit.runners.JUnit4
10+
11+
import scala.tools.nsc.Settings
12+
import scala.tools.nsc.backend.jvm.AsmUtils
13+
import scala.tools.nsc.util.ClassPath
14+
import scala.tools.util.PathResolver
15+
16+
@RunWith(classOf[JUnit4])
17+
class JrtClassPathTest {
18+
19+
@Test def lookupJavaClasses(): Unit = {
20+
val specVersion = scala.util.Properties.javaSpecVersion
21+
// Run the test using the JDK8 or 9 provider for rt.jar depending on the platform the test is running on.
22+
val cp: ClassPath =
23+
if (specVersion == "" || specVersion == "1.8") {
24+
val settings = new Settings()
25+
val resolver = new PathResolver(settings)
26+
val elements = new ClassPathFactory(settings).classesInPath(resolver.Calculated.javaBootClassPath)
27+
AggregateClassPath(elements)
28+
}
29+
else JrtClassPath().get
30+
31+
assertEquals(Nil, cp.classes(""))
32+
assertTrue(cp.packages("java").toString, cp.packages("java").exists(_.name == "java.lang"))
33+
assertTrue(cp.classes("java.lang").exists(_.name == "Object"))
34+
val jl_Object = cp.classes("java.lang").find(_.name == "Object").get
35+
assertEquals("java/lang/Object", AsmUtils.classFromBytes(jl_Object.file.toByteArray).name)
36+
assertTrue(cp.list("java.lang").packages.exists(_.name == "java.lang.annotation"))
37+
assertTrue(cp.list("java.lang").classesAndSources.exists(_.name == "Object"))
38+
assertTrue(cp.findClass("java.lang.Object").isDefined)
39+
assertTrue(cp.findClassFile("java.lang.Object").isDefined)
40+
}
41+
}

0 commit comments

Comments
 (0)