Skip to content

Commit 481ad48

Browse files
committed
Optimize classpath implementation to speed up SBT
Adapted from scala commits b9520211b22e53d4f80801b0f64cdb6275ac55cf and bd7341754f8aec4ddbf455544eb393ff6cf4a43a by Jason Zaugg and Lukas Rytz in scala/scala#5956: - Rather than `findAll.filter(_.name == name)`, just lookup the entry with the right name to start with. - Avoid a linear scan of aggregate classpath by building and index keyed on package names
1 parent 2516575 commit 481ad48

File tree

6 files changed

+87
-33
lines changed

6 files changed

+87
-33
lines changed

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

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,32 @@ import dotty.tools.io.{ AbstractFile, ClassPath, ClassRepresentation }
1818
* @param aggregates classpath instances containing entries which this class processes
1919
*/
2020
case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
21+
22+
private[this] val packageIndex: collection.mutable.Map[String, Seq[ClassPath]] = collection.mutable.Map()
23+
private def aggregatesForPackage(pkg: String): Seq[ClassPath] = packageIndex.synchronized {
24+
packageIndex.getOrElseUpdate(pkg, aggregates.filter(_.hasPackage(pkg)))
25+
}
26+
2127
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)
28+
val (pkg, _) = PackageNameUtils.separatePkgAndClassNames(className)
29+
aggregatesForPackage(pkg).iterator.map(_.findClassFile(className)).collectFirst {
30+
case Some(x) => x
31+
}
3132
}
3233

34+
// This method is performance sensitive as it is used by SBT's ExtractDependencies phase.
3335
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)
36+
val (pkg, _) = PackageNameUtils.separatePkgAndClassNames(className)
37+
38+
def findEntry(isSource: Boolean): Option[ClassRepresentation] = {
39+
aggregatesForPackage(pkg).iterator.map(_.findClass(className)).collectFirst {
40+
case Some(s: SourceFileEntry) if isSource => s
41+
case Some(s: ClassFileEntry) if !isSource => s
42+
}
43+
}
44+
45+
val classEntry = findEntry(isSource = false)
46+
val sourceEntry = findEntry(isSource = true)
4847

4948
(classEntry, sourceEntry) match {
5049
case (Some(c: ClassFileEntry), Some(s: SourceFileEntry)) => Some(ClassAndSourceFilesEntry(c.file, s.file))
@@ -70,6 +69,7 @@ case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
7069
override private[dotty] def sources(inPackage: String): Seq[SourceFileEntry] =
7170
getDistinctEntries(_.sources(inPackage))
7271

72+
override private[dotty] def hasPackage(pkg: String) = aggregates.exists(_.hasPackage(pkg))
7373
override private[dotty] def list(inPackage: String): ClassPathEntries = {
7474
val (packages, classesAndSources) = aggregates.map { cp =>
7575
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: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ package dotty.tools.dotc.classpath
66
import java.io.File
77
import java.net.URL
88
import scala.annotation.tailrec
9-
import dotty.tools.io.{AbstractFile, ClassPath, FileZipArchive, ManifestResources}
10-
import dotty.tools.dotc.config.Settings
9+
import dotty.tools.io.{AbstractFile, ClassPath, ClassRepresentation, FileZipArchive, ManifestResources}
1110
import dotty.tools.dotc.core.Contexts.Context
1211
import FileUtils._
1312

@@ -50,7 +49,13 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
5049

5150
override def findClassFile(className: String): Option[AbstractFile] = {
5251
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
53-
classes(pkg).find(_.name == simpleClassName).map(_.file)
52+
file(pkg, simpleClassName + ".class").map(_.file)
53+
}
54+
55+
// This method is performance sensitive as it is used by SBT's ExtractDependencies phase.
56+
override def findClass(className: String): Option[ClassRepresentation] = {
57+
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
58+
file(pkg, simpleClassName + ".class")
5459
}
5560

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

141+
override private[dotty] def hasPackage(pkg: String) = cachedPackages.contains(pkg)
136142
override private[dotty] def list(inPackage: String): ClassPathEntries = ClassPathEntries(packages(inPackage), classes(inPackage))
137143
}
138144

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

0 commit comments

Comments
 (0)