Skip to content

Commit 42b055a

Browse files
committed
Optimize the classpath implementation
- Avoid repeated conversion between package dotted names ("com.foo") and relative paths ("com/foo/") by threading a `PackageName` instance through the aggregate classpath lookup which caches the path name. - Avoid creating result buffers inside of each element of the aggregate classpath by introducing a callback based API that lets `AggregateClasspath` use a single buffer.
1 parent 33f11d3 commit 42b055a

File tree

11 files changed

+177
-141
lines changed

11 files changed

+177
-141
lines changed

src/compiler/scala/tools/nsc/backend/jvm/ClassfileWriters.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
package scala.tools.nsc.backend.jvm
1414

15-
import java.io.{BufferedOutputStream, DataOutputStream, FileOutputStream, IOException}
15+
import java.io.{DataOutputStream, IOException}
1616
import java.nio.ByteBuffer
1717
import java.nio.channels.{ClosedByInterruptException, FileChannel}
1818
import java.nio.charset.StandardCharsets

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

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
package scala.tools.nsc.classpath
1414

1515
import java.net.URL
16+
1617
import scala.collection.mutable.ArrayBuffer
1718
import scala.reflect.internal.FatalError
1819
import scala.reflect.io.AbstractFile
19-
import scala.tools.nsc.util.ClassPath
20-
import scala.tools.nsc.util.ClassRepresentation
20+
import scala.tools.nsc.util.{ClassPath, ClassRepresentation, EfficientClassPath}
2121

2222
/**
2323
* A classpath unifying multiple class- and sourcepath entries.
@@ -30,21 +30,21 @@ import scala.tools.nsc.util.ClassRepresentation
3030
case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
3131
override def findClassFile(className: String): Option[AbstractFile] = {
3232
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
33-
aggregatesForPackage(pkg).iterator.map(_.findClassFile(className)).collectFirst {
33+
aggregatesForPackage(PackageName(pkg)).iterator.map(_.findClassFile(className)).collectFirst {
3434
case Some(x) => x
3535
}
3636
}
3737
private[this] val packageIndex: collection.mutable.Map[String, Seq[ClassPath]] = collection.mutable.Map()
38-
private def aggregatesForPackage(pkg: String): Seq[ClassPath] = packageIndex.synchronized {
39-
packageIndex.getOrElseUpdate(pkg, aggregates.filter(_.hasPackage(pkg)))
38+
private def aggregatesForPackage(pkg: PackageName): Seq[ClassPath] = packageIndex.synchronized {
39+
packageIndex.getOrElseUpdate(pkg.dottedString, aggregates.filter(_.hasPackage(pkg)))
4040
}
4141

4242
// This method is performance sensitive as it is used by SBT's ExtractDependencies phase.
4343
override def findClass(className: String): Option[ClassRepresentation] = {
4444
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
4545

4646
def findEntry(isSource: Boolean): Option[ClassRepresentation] = {
47-
aggregatesForPackage(pkg).iterator.map(_.findClass(className)).collectFirst {
47+
aggregatesForPackage(PackageName(pkg)).iterator.map(_.findClass(className)).collectFirst {
4848
case Some(s: SourceFileEntry) if isSource => s
4949
case Some(s: ClassFileEntry) if !isSource => s
5050
}
@@ -66,31 +66,41 @@ case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
6666

6767
override def asSourcePathString: String = ClassPath.join(aggregates map (_.asSourcePathString): _*)
6868

69-
override private[nsc] def packages(inPackage: String): Seq[PackageEntry] = {
69+
override private[nsc] def packages(inPackage: PackageName): Seq[PackageEntry] = {
7070
val aggregatedPackages = aggregates.flatMap(_.packages(inPackage)).distinct
7171
aggregatedPackages
7272
}
7373

74-
override private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] =
74+
override private[nsc] def classes(inPackage: PackageName): Seq[ClassFileEntry] =
7575
getDistinctEntries(_.classes(inPackage))
7676

77-
override private[nsc] def sources(inPackage: String): Seq[SourceFileEntry] =
77+
override private[nsc] def sources(inPackage: PackageName): Seq[SourceFileEntry] =
7878
getDistinctEntries(_.sources(inPackage))
7979

80-
override private[nsc] def hasPackage(pkg: String) = aggregates.exists(_.hasPackage(pkg))
81-
override private[nsc] def list(inPackage: String): ClassPathEntries = {
82-
val (packages, classesAndSources) = aggregates.map { cp =>
80+
override private[nsc] def hasPackage(pkg: PackageName) = aggregates.exists(_.hasPackage(pkg))
81+
override private[nsc] def list(inPackage: PackageName): ClassPathEntries = {
82+
val packages: java.util.HashSet[PackageEntry] = new java.util.HashSet[PackageEntry]()
83+
val classesAndSourcesBuffer = collection.mutable.ArrayBuffer[ClassRepresentation]()
84+
aggregates.foreach { cp =>
8385
try {
84-
cp.list(inPackage)
86+
cp match {
87+
case ecp: EfficientClassPath =>
88+
ecp.list(inPackage, packages.add(_), classesAndSourcesBuffer += _)
89+
case _ =>
90+
val entries = cp.list(inPackage)
91+
entries._1.foreach(entry => packages.add(entry))
92+
classesAndSourcesBuffer ++= entries._2
93+
}
8594
} catch {
8695
case ex: java.io.IOException =>
8796
val e = FatalError(ex.getMessage)
8897
e.initCause(ex)
8998
throw e
9099
}
91-
}.unzip
92-
val distinctPackages = packages.flatten.distinct
93-
val distinctClassesAndSources = mergeClassesAndSources(classesAndSources)
100+
}
101+
102+
val distinctPackages: Seq[PackageEntry] = if (packages == null) Nil else packages.toArray(new Array[PackageEntry](packages.size()))
103+
val distinctClassesAndSources = mergeClassesAndSources(classesAndSourcesBuffer)
94104
ClassPathEntries(distinctPackages, distinctClassesAndSources)
95105
}
96106

@@ -99,14 +109,12 @@ case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
99109
* creates an entry containing both of them. If there would be more than one class or source
100110
* entries for the same class it always would use the first entry of each type found on a classpath.
101111
*/
102-
private def mergeClassesAndSources(entries: Seq[Seq[ClassRepresentation]]): Seq[ClassRepresentation] = {
112+
private def mergeClassesAndSources(entries: Seq[ClassRepresentation]): Seq[ClassRepresentation] = {
103113
var count = 0
104114
val indices = collection.mutable.HashMap[String, Int]()
105-
val mergedEntries = new ArrayBuffer[ClassRepresentation](1024)
106-
115+
val mergedEntries = new ArrayBuffer[ClassRepresentation](entries.size)
107116
for {
108-
partOfEntries <- entries
109-
entry <- partOfEntries
117+
entry <- entries
110118
} {
111119
val name = entry.name
112120
if (indices contains name) {

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ trait SourceFileEntry extends ClassRepresentation {
3232
def file: AbstractFile
3333
}
3434

35+
case class PackageName(dottedString: String) {
36+
def isRoot: Boolean = dottedString.isEmpty
37+
val dirPathTrailingSlash: String = FileUtils.dirPath(dottedString) + "/"
38+
39+
def entryName(entry: String): String = {
40+
if (isRoot) entry else {
41+
val builder = new java.lang.StringBuilder(dottedString.length + 1 + entry.length)
42+
builder.append(dottedString)
43+
builder.append('.')
44+
builder.append(entry)
45+
builder.toString
46+
}
47+
}
48+
}
49+
3550
trait PackageEntry {
3651
def name: String
3752
}
@@ -61,10 +76,10 @@ private[nsc] case class PackageEntryImpl(name: String) extends PackageEntry
6176

6277
private[nsc] trait NoSourcePaths {
6378
final def asSourcePathString: String = ""
64-
final private[nsc] def sources(inPackage: String): Seq[SourceFileEntry] = Seq.empty
79+
final private[nsc] def sources(inPackage: PackageName): Seq[SourceFileEntry] = Seq.empty
6580
}
6681

6782
private[nsc] trait NoClassPaths {
6883
final def findClassFile(className: String): Option[AbstractFile] = None
69-
private[nsc] final def classes(inPackage: String): Seq[ClassFileEntry] = Seq.empty
84+
private[nsc] final def classes(inPackage: PackageName): Seq[ClassFileEntry] = Seq.empty
7085
}

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

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import java.nio.file.{FileSystems, Files}
1818
import java.util
1919

2020
import scala.reflect.io.{AbstractFile, PlainFile, PlainNioFile}
21-
import scala.tools.nsc.util.{ClassPath, ClassRepresentation}
21+
import scala.tools.nsc.util.{ClassPath, ClassRepresentation, EfficientClassPath}
2222
import FileUtils._
2323
import scala.collection.JavaConverters._
2424
import scala.reflect.internal.JDK9Reflectors
@@ -32,7 +32,7 @@ import scala.tools.nsc.classpath.PackageNameUtils.{packageContains, separatePkgA
3232
* when we have a name of a package.
3333
* It abstracts over the file representation to work with both JFile and AbstractFile.
3434
*/
35-
trait DirectoryLookup[FileEntryType <: ClassRepresentation] extends ClassPath {
35+
trait DirectoryLookup[FileEntryType <: ClassRepresentation] extends EfficientClassPath {
3636
type F
3737

3838
val dir: F
@@ -47,28 +47,26 @@ trait DirectoryLookup[FileEntryType <: ClassRepresentation] extends ClassPath {
4747
protected def createFileEntry(file: AbstractFile): FileEntryType
4848
protected def isMatchingFile(f: F): Boolean
4949

50-
private def getDirectory(forPackage: String): Option[F] = {
51-
if (forPackage == ClassPath.RootPackage) {
50+
private def getDirectory(forPackage: PackageName): Option[F] = {
51+
if (forPackage.isRoot) {
5252
Some(dir)
5353
} else {
54-
val packageDirName = FileUtils.dirPath(forPackage)
55-
getSubDir(packageDirName)
54+
getSubDir(forPackage.dirPathTrailingSlash)
5655
}
5756
}
58-
override private[nsc] def hasPackage(pkg: String) = getDirectory(pkg).isDefined
57+
override private[nsc] def hasPackage(pkg: PackageName) = getDirectory(pkg).isDefined
5958

60-
private[nsc] def packages(inPackage: String): Seq[PackageEntry] = {
59+
private[nsc] def packages(inPackage: PackageName): Seq[PackageEntry] = {
6160
val dirForPackage = getDirectory(inPackage)
6261

6362
val nestedDirs: Array[F] = dirForPackage match {
6463
case None => emptyFiles
6564
case Some(directory) => listChildren(directory, Some(isPackage))
6665
}
67-
val prefix = PackageNameUtils.packagePrefix(inPackage)
68-
nestedDirs.map(f => PackageEntryImpl(prefix + getName(f)))
66+
nestedDirs.map(f => PackageEntryImpl(inPackage.entryName(getName(f))))
6967
}
7068

71-
protected def files(inPackage: String): Seq[FileEntryType] = {
69+
protected def files(inPackage: PackageName): Seq[FileEntryType] = {
7270
val dirForPackage = getDirectory(inPackage)
7371
val files: Array[F] = dirForPackage match {
7472
case None => emptyFiles
@@ -77,22 +75,18 @@ trait DirectoryLookup[FileEntryType <: ClassRepresentation] extends ClassPath {
7775
files.map(f => createFileEntry(toAbstractFile(f)))
7876
}
7977

80-
private[nsc] def list(inPackage: String): ClassPathEntries = {
78+
override private[nsc] def list(inPackage: PackageName, onPackageEntry: PackageEntry => Unit, onClassesAndSources: ClassRepresentation => Unit): Unit = {
8179
val dirForPackage = getDirectory(inPackage)
82-
val files: Array[F] = dirForPackage match {
83-
case None => emptyFiles
84-
case Some(directory) => listChildren(directory)
85-
}
86-
val packagePrefix = PackageNameUtils.packagePrefix(inPackage)
87-
val packageBuf = collection.mutable.ArrayBuffer.empty[PackageEntry]
88-
val fileBuf = collection.mutable.ArrayBuffer.empty[FileEntryType]
89-
for (file <- files) {
90-
if (isPackage(file))
91-
packageBuf += PackageEntryImpl(packagePrefix + getName(file))
92-
else if (isMatchingFile(file))
93-
fileBuf += createFileEntry(toAbstractFile(file))
80+
dirForPackage match {
81+
case None =>
82+
case Some(directory) =>
83+
for (file <- listChildren(directory)) {
84+
if (isPackage(file))
85+
onPackageEntry(PackageEntryImpl(inPackage.entryName(getName(file))))
86+
else if (isMatchingFile(file))
87+
onClassesAndSources(createFileEntry(toAbstractFile(file)))
88+
}
9489
}
95-
ClassPathEntries(packageBuf, fileBuf)
9690
}
9791
}
9892

@@ -196,21 +190,21 @@ final class JrtClassPath(fs: java.nio.file.FileSystem) extends ClassPath with No
196190
}
197191

198192
/** Empty string represents root package */
199-
override private[nsc] def hasPackage(pkg: String) = packageToModuleBases.contains(pkg)
200-
override private[nsc] def packages(inPackage: String): Seq[PackageEntry] = {
201-
packageToModuleBases.keysIterator.filter(pack => packageContains(inPackage, pack)).map(PackageEntryImpl(_)).toVector
193+
override private[nsc] def hasPackage(pkg: PackageName) = packageToModuleBases.contains(pkg.dottedString)
194+
override private[nsc] def packages(inPackage: PackageName): Seq[PackageEntry] = {
195+
packageToModuleBases.keysIterator.filter(pack => packageContains(inPackage.dottedString, pack)).map(PackageEntryImpl(_)).toVector
202196
}
203-
private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = {
204-
if (inPackage == "") Nil
197+
private[nsc] def classes(inPackage: PackageName): Seq[ClassFileEntry] = {
198+
if (inPackage.isRoot) Nil
205199
else {
206-
packageToModuleBases.getOrElse(inPackage, Nil).flatMap(x =>
207-
Files.list(x.resolve(inPackage.replace('.', '/'))).iterator().asScala.filter(_.getFileName.toString.endsWith(".class"))).map(x =>
200+
packageToModuleBases.getOrElse(inPackage.dottedString, Nil).flatMap(x =>
201+
Files.list(x.resolve(inPackage.dirPathTrailingSlash)).iterator().asScala.filter(_.getFileName.toString.endsWith(".class"))).map(x =>
208202
ClassFileEntryImpl(new PlainNioFile(x))).toVector
209203
}
210204
}
211205

212-
override private[nsc] def list(inPackage: String): ClassPathEntries =
213-
if (inPackage == "") ClassPathEntries(packages(inPackage), Nil)
206+
override private[nsc] def list(inPackage: PackageName): ClassPathEntries =
207+
if (inPackage.isRoot) ClassPathEntries(packages(inPackage), Nil)
214208
else ClassPathEntries(packages(inPackage), classes(inPackage))
215209

216210
def asURLs: Seq[URL] = Seq(new URL("jrt:/"))
@@ -262,21 +256,21 @@ final class CtSymClassPath(ctSym: java.nio.file.Path, release: Int) extends Clas
262256
}
263257

264258
/** Empty string represents root package */
265-
override private[nsc] def hasPackage(pkg: String) = packageIndex.contains(pkg)
266-
override private[nsc] def packages(inPackage: String): Seq[PackageEntry] = {
267-
packageIndex.keysIterator.filter(pack => packageContains(inPackage, pack)).map(PackageEntryImpl(_)).toVector
259+
override private[nsc] def hasPackage(pkg: PackageName) = packageIndex.contains(pkg.dottedString)
260+
override private[nsc] def packages(inPackage: PackageName): Seq[PackageEntry] = {
261+
packageIndex.keysIterator.filter(pack => packageContains(inPackage.dottedString, pack)).map(PackageEntryImpl(_)).toVector
268262
}
269-
private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = {
270-
if (inPackage == "") Nil
263+
private[nsc] def classes(inPackage: PackageName): Seq[ClassFileEntry] = {
264+
if (inPackage.isRoot) Nil
271265
else {
272-
val sigFiles = packageIndex.getOrElse(inPackage, Nil).iterator.flatMap(p =>
266+
val sigFiles = packageIndex.getOrElse(inPackage.dottedString, Nil).iterator.flatMap(p =>
273267
Files.list(p).iterator().asScala.filter(_.getFileName.toString.endsWith(".sig")))
274268
sigFiles.map(f => ClassFileEntryImpl(new PlainNioFile(f))).toVector
275269
}
276270
}
277271

278-
override private[nsc] def list(inPackage: String): ClassPathEntries =
279-
if (inPackage == "") ClassPathEntries(packages(inPackage), Nil)
272+
override private[nsc] def list(inPackage: PackageName): ClassPathEntries =
273+
if (inPackage.isRoot) ClassPathEntries(packages(inPackage), Nil)
280274
else ClassPathEntries(packages(inPackage), classes(inPackage))
281275

282276
def asURLs: Seq[URL] = Nil
@@ -310,7 +304,7 @@ case class DirectoryClassPath(dir: File) extends JFileDirectoryLookup[ClassFileE
310304
protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
311305
protected def isMatchingFile(f: File): Boolean = f.isClass
312306

313-
private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = files(inPackage)
307+
private[nsc] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
314308
}
315309

316310
case class DirectorySourcePath(dir: File) extends JFileDirectoryLookup[SourceFileEntryImpl] with NoClassPaths {
@@ -334,5 +328,5 @@ case class DirectorySourcePath(dir: File) extends JFileDirectoryLookup[SourceFil
334328
}
335329
}
336330

337-
private[nsc] def sources(inPackage: String): Seq[SourceFileEntry] = files(inPackage)
331+
private[nsc] def sources(inPackage: PackageName): Seq[SourceFileEntry] = files(inPackage)
338332
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ case class VirtualDirectoryClassPath(dir: VirtualDirectory) extends ClassPath wi
4545
Option(AbstractFileClassLoader.lookupPath(dir)(relativePath split '/', directory = false))
4646
}
4747

48-
private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = files(inPackage)
48+
private[nsc] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
4949

5050
protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
5151
protected def isMatchingFile(f: AbstractFile): Boolean = f.isClass

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

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,15 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
5959

6060
override def findClassFile(className: String): Option[AbstractFile] = {
6161
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
62-
file(pkg, simpleClassName + ".class").map(_.file)
62+
file(PackageName(pkg), simpleClassName + ".class").map(_.file)
6363
}
6464
// This method is performance sensitive as it is used by SBT's ExtractDependencies phase.
6565
override def findClass(className: String): Option[ClassRepresentation] = {
6666
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
67-
file(pkg, simpleClassName + ".class")
67+
file(PackageName(pkg), simpleClassName + ".class")
6868
}
6969

70-
override private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = files(inPackage)
70+
override private[nsc] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
7171

7272
override protected def createFileEntry(file: FileZipArchive#Entry): ClassFileEntryImpl = ClassFileEntryImpl(file)
7373
override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isClass
@@ -83,7 +83,7 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
8383
private case class ManifestResourcesClassPath(file: ManifestResources) extends ClassPath with NoSourcePaths with Closeable {
8484
override def findClassFile(className: String): Option[AbstractFile] = {
8585
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
86-
classes(pkg).find(_.name == simpleClassName).map(_.file)
86+
classes(PackageName(pkg)).find(_.name == simpleClassName).map(_.file)
8787
}
8888

8989
override def asClassPathStrings: Seq[String] = Seq(file.path)
@@ -135,22 +135,21 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
135135
packages
136136
}
137137

138-
override private[nsc] def packages(inPackage: String): Seq[PackageEntry] = cachedPackages.get(inPackage) match {
138+
override private[nsc] def packages(inPackage: PackageName): Seq[PackageEntry] = cachedPackages.get(inPackage.dottedString) match {
139139
case None => Seq.empty
140140
case Some(PackageFileInfo(_, subpackages)) =>
141-
val prefix = PackageNameUtils.packagePrefix(inPackage)
142-
subpackages.map(packageFile => PackageEntryImpl(prefix + packageFile.name))
141+
subpackages.map(packageFile => PackageEntryImpl(inPackage.entryName(packageFile.name)))
143142
}
144143

145-
override private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = cachedPackages.get(inPackage) match {
144+
override private[nsc] def classes(inPackage: PackageName): Seq[ClassFileEntry] = cachedPackages.get(inPackage.dottedString) match {
146145
case None => Seq.empty
147146
case Some(PackageFileInfo(pkg, _)) =>
148147
(for (file <- pkg if file.isClass) yield ClassFileEntryImpl(file))(collection.breakOut)
149148
}
150149

151150

152-
override private[nsc] def hasPackage(pkg: String) = cachedPackages.contains(pkg)
153-
override private[nsc] def list(inPackage: String): ClassPathEntries = ClassPathEntries(packages(inPackage), classes(inPackage))
151+
override private[nsc] def hasPackage(pkg: PackageName) = cachedPackages.contains(pkg.dottedString)
152+
override private[nsc] def list(inPackage: PackageName): ClassPathEntries = ClassPathEntries(packages(inPackage), classes(inPackage))
154153
}
155154

156155
private object ManifestResourcesClassPath {
@@ -183,7 +182,7 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
183182

184183
override def asSourcePathString: String = asClassPathString
185184

186-
override private[nsc] def sources(inPackage: String): Seq[SourceFileEntry] = files(inPackage)
185+
override private[nsc] def sources(inPackage: PackageName): Seq[SourceFileEntry] = files(inPackage)
187186

188187
override protected def createFileEntry(file: FileZipArchive#Entry): SourceFileEntryImpl = SourceFileEntryImpl(file)
189188
override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isScalaOrJavaSource

0 commit comments

Comments
 (0)