Skip to content

Commit 484d6d7

Browse files
committed
SI-6240 introduces GIL to Scala reflection
On a serious note, I feel really uncomfortable about having to juggle this slew of locks. Despite that I can't immediately find a deadlock, I'm 100% sure there is one hiding in the shadows. Hence, I'm abandoning all runtime reflection locks in favor of a single per-universe one.
1 parent f7c6213 commit 484d6d7

10 files changed

+226
-111
lines changed

src/reflect/scala/reflect/internal/BaseTypeSeqs.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ trait BaseTypeSeqs {
3838
* This is necessary because when run from reflection every base type sequence needs to have a
3939
* SynchronizedBaseTypeSeq as mixin.
4040
*/
41-
class BaseTypeSeq protected[BaseTypeSeqs] (private[BaseTypeSeqs] val parents: List[Type], private[BaseTypeSeqs] val elems: Array[Type]) {
41+
class BaseTypeSeq protected[reflect] (private[BaseTypeSeqs] val parents: List[Type], private[BaseTypeSeqs] val elems: Array[Type]) {
4242
self =>
4343
if (Statistics.canEnable) Statistics.incCounter(baseTypeSeqCount)
4444
if (Statistics.canEnable) Statistics.incCounter(baseTypeSeqLenTotal, elems.length)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package scala.reflect
2+
package runtime
3+
4+
private[reflect] trait Gil {
5+
self: SymbolTable =>
6+
7+
// fixme... please...
8+
// there are the following avenues of optimization we discussed with Roland:
9+
// 1) replace PackageScope locks with ConcurrentHashMap, because PackageScope materializers seem to be idempotent
10+
// 2) unlock unpickling completers by verifying that they are idempotent or moving non-idempotent parts
11+
// 3) remove the necessity in global state for isSubType
12+
lazy val gil = new java.util.concurrent.locks.ReentrantLock
13+
14+
@inline final def gilSynchronized[T](body: => T): T = {
15+
try {
16+
gil.lock()
17+
body
18+
} finally {
19+
gil.unlock()
20+
}
21+
}
22+
}

src/reflect/scala/reflect/runtime/JavaMirrors.scala

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import ReflectionUtils.{staticSingletonInstance, innerSingletonInstance, scalacS
2222
import scala.language.existentials
2323
import scala.runtime.{ScalaRunTime, BoxesRunTime}
2424

25-
private[reflect] trait JavaMirrors extends internal.SymbolTable with api.JavaUniverse { thisUniverse: SymbolTable =>
25+
private[reflect] trait JavaMirrors extends internal.SymbolTable with api.JavaUniverse with TwoWayCaches { thisUniverse: SymbolTable =>
2626

2727
private lazy val mirrors = new WeakHashMap[ClassLoader, WeakReference[JavaMirror]]()
2828

@@ -44,9 +44,11 @@ private[reflect] trait JavaMirrors extends internal.SymbolTable with api.JavaUni
4444

4545
trait JavaClassCompleter extends FlagAssigningCompleter
4646

47-
def runtimeMirror(cl: ClassLoader): Mirror = mirrors get cl match {
48-
case Some(WeakReference(m)) => m
49-
case _ => createMirror(rootMirror.RootClass, cl)
47+
def runtimeMirror(cl: ClassLoader): Mirror = gilSynchronized {
48+
mirrors get cl match {
49+
case Some(WeakReference(m)) => m
50+
case _ => createMirror(rootMirror.RootClass, cl)
51+
}
5052
}
5153

5254
/** The API of a mirror for a reflective universe */
@@ -684,7 +686,7 @@ private[reflect] trait JavaMirrors extends internal.SymbolTable with api.JavaUni
684686
completeRest()
685687
}
686688

687-
def completeRest(): Unit = thisUniverse.synchronized {
689+
def completeRest(): Unit = gilSynchronized {
688690
val tparams = clazz.rawInfo.typeParams
689691

690692
val parents = try {
@@ -889,7 +891,7 @@ private[reflect] trait JavaMirrors extends internal.SymbolTable with api.JavaUni
889891
* The Scala package with given fully qualified name. Unlike `packageNameToScala`,
890892
* this one bypasses the cache.
891893
*/
892-
private[JavaMirrors] def makeScalaPackage(fullname: String): ModuleSymbol = {
894+
private[JavaMirrors] def makeScalaPackage(fullname: String): ModuleSymbol = gilSynchronized {
893895
val split = fullname lastIndexOf '.'
894896
val ownerModule: ModuleSymbol =
895897
if (split > 0) packageNameToScala(fullname take split) else this.RootPackage

src/reflect/scala/reflect/runtime/JavaUniverseForce.scala

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ trait JavaUniverseForce { self: runtime.JavaUniverse =>
2727

2828
this.settings
2929
this.treeInfo
30+
this.gil
3031
// inaccessible: this.uniqueLock
31-
// inaccessible: this.subsametypeLock
32-
// inaccessible: this.lubglbLock
33-
// inaccessible: this.indentLock
34-
// inaccessible: this.toStringLock
32+
// inaccessible: this.nextIdLock
33+
// inaccessible: this.freshExistentialNameLock
3534
// inaccessible: this.mirrors
3635
this.rootMirror
3736
this.treeBuild

src/reflect/scala/reflect/runtime/SymbolLoaders.scala

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,25 @@ private[reflect] trait SymbolLoaders { self: SymbolTable =>
6767
}
6868
}
6969

70+
71+
// Since runtime reflection doesn't have a luxury of enumerating all classes
72+
// on the classpath, it has to materialize symbols for top-level definitions
73+
// (packages, classes, objects) on demand.
74+
//
75+
// Someone asks us for a class named `foo.Bar`? Easy. Let's speculatively create
76+
// a package named `foo` and then look up `newTypeName("bar")` in its decls.
77+
// This lookup, implemented in `SymbolLoaders.PackageScope` tests the waters by
78+
// trying to to `Class.forName("foo.Bar")` and then creates a ClassSymbol upon
79+
// success (the whole story is a bit longer, but the rest is irrelevant here).
80+
//
81+
// That's all neat, but these non-deterministic mutations of the global symbol
82+
// table give a lot of trouble in multi-threaded setting. One of the popular
83+
// reflection crashes happens when multiple threads happen to trigger symbol
84+
// materialization multiple times for the same symbol, making subsequent
85+
// reflective operations stumble upon outrageous stuff like overloaded packages.
86+
//
87+
// Short of significantly changing SymbolLoaders I see no other way than just
88+
// to slap a global lock on materialization in runtime reflection.
7089
class PackageScope(pkgClass: Symbol) extends Scope(initFingerPrints = -1L) // disable fingerprinting as we do not know entries beforehand
7190
with SynchronizedScope {
7291
assert(pkgClass.isType)
@@ -89,9 +108,11 @@ private[reflect] trait SymbolLoaders { self: SymbolTable =>
89108
else existing.sym.asInstanceOf[T]
90109
}
91110

92-
// disable fingerprinting as we do not know entries beforehand
93-
private val negatives = mutable.Set[Name]() // Syncnote: Performance only, so need not be protected.
94-
override def lookupEntry(name: Name): ScopeEntry = {
111+
// package scopes need to synchronize on the GIL
112+
// because lookupEntry might cause changes to the global symbol table
113+
override def syncLockSynchronized[T](body: => T): T = gilSynchronized(body)
114+
private val negatives = new mutable.HashSet[Name]
115+
override def lookupEntry(name: Name): ScopeEntry = syncLockSynchronized {
95116
val e = super.lookupEntry(name)
96117
if (e != null)
97118
e

src/reflect/scala/reflect/runtime/SymbolTable.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import scala.reflect.internal.Flags._
99
* It can be used either from a reflexive universe (class scala.reflect.runtime.JavaUniverse), or else from
1010
* a runtime compiler that uses reflection to get a class information (class scala.tools.reflect.ReflectGlobal)
1111
*/
12-
private[scala] trait SymbolTable extends internal.SymbolTable with JavaMirrors with SymbolLoaders with SynchronizedOps {
12+
private[scala] trait SymbolTable extends internal.SymbolTable with JavaMirrors with SymbolLoaders with SynchronizedOps with Gil {
1313

1414
def info(msg: => String) =
1515
if (settings.verbose) println("[reflect-compiler] "+msg)

src/reflect/scala/reflect/runtime/SynchronizedOps.scala

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,25 @@ private[reflect] trait SynchronizedOps extends internal.SymbolTable
1414
// BaseTypeSeqs
1515

1616
override protected def newBaseTypeSeq(parents: List[Type], elems: Array[Type]) =
17-
new BaseTypeSeq(parents, elems) with SynchronizedBaseTypeSeq
17+
// only need to synchronize BaseTypeSeqs if they contain refined types
18+
if (elems.filter(_.isInstanceOf[RefinedType]).nonEmpty) new BaseTypeSeq(parents, elems) with SynchronizedBaseTypeSeq
19+
else new BaseTypeSeq(parents, elems)
1820

1921
trait SynchronizedBaseTypeSeq extends BaseTypeSeq {
20-
override def apply(i: Int): Type = synchronized { super.apply(i) }
21-
override def rawElem(i: Int) = synchronized { super.rawElem(i) }
22-
override def typeSymbol(i: Int): Symbol = synchronized { super.typeSymbol(i) }
23-
override def toList: List[Type] = synchronized { super.toList }
24-
override def copy(head: Type, offset: Int): BaseTypeSeq = synchronized { super.copy(head, offset) }
25-
override def map(f: Type => Type): BaseTypeSeq = synchronized { super.map(f) }
26-
override def exists(p: Type => Boolean): Boolean = synchronized { super.exists(p) }
27-
override lazy val maxDepth = synchronized { maxDepthOfElems }
28-
override def toString = synchronized { super.toString }
29-
30-
override def lateMap(f: Type => Type): BaseTypeSeq = new MappedBaseTypeSeq(this, f) with SynchronizedBaseTypeSeq
22+
override def apply(i: Int): Type = gilSynchronized { super.apply(i) }
23+
override def rawElem(i: Int) = gilSynchronized { super.rawElem(i) }
24+
override def typeSymbol(i: Int): Symbol = gilSynchronized { super.typeSymbol(i) }
25+
override def toList: List[Type] = gilSynchronized { super.toList }
26+
override def copy(head: Type, offset: Int): BaseTypeSeq = gilSynchronized { super.copy(head, offset) }
27+
override def map(f: Type => Type): BaseTypeSeq = gilSynchronized { super.map(f) }
28+
override def exists(p: Type => Boolean): Boolean = gilSynchronized { super.exists(p) }
29+
override lazy val maxDepth = gilSynchronized { maxDepthOfElems }
30+
override def toString = gilSynchronized { super.toString }
31+
32+
override def lateMap(f: Type => Type): BaseTypeSeq =
33+
// only need to synchronize BaseTypeSeqs if they contain refined types
34+
if (map(f).toList.filter(_.isInstanceOf[RefinedType]).nonEmpty) new MappedBaseTypeSeq(this, f) with SynchronizedBaseTypeSeq
35+
else new MappedBaseTypeSeq(this, f)
3136
}
3237

3338
// Scopes
@@ -36,15 +41,19 @@ private[reflect] trait SynchronizedOps extends internal.SymbolTable
3641
override def newNestedScope(outer: Scope): Scope = new Scope(outer) with SynchronizedScope
3742

3843
trait SynchronizedScope extends Scope {
39-
override def isEmpty: Boolean = synchronized { super.isEmpty }
40-
override def size: Int = synchronized { super.size }
41-
override def enter[T <: Symbol](sym: T): T = synchronized { super.enter(sym) }
42-
override def rehash(sym: Symbol, newname: Name) = synchronized { super.rehash(sym, newname) }
43-
override def unlink(e: ScopeEntry) = synchronized { super.unlink(e) }
44-
override def unlink(sym: Symbol) = synchronized { super.unlink(sym) }
45-
override def lookupAll(name: Name) = synchronized { super.lookupAll(name) }
46-
override def lookupEntry(name: Name) = synchronized { super.lookupEntry(name) }
47-
override def lookupNextEntry(entry: ScopeEntry) = synchronized { super.lookupNextEntry(entry) }
48-
override def toList: List[Symbol] = synchronized { super.toList }
44+
// we can keep this lock fine-grained, because methods of Scope don't do anything extraordinary, which makes deadlocks impossible
45+
// fancy subclasses of internal.Scopes#Scope should do synchronization themselves (e.g. see PackageScope for an example)
46+
private lazy val syncLock = new Object
47+
def syncLockSynchronized[T](body: => T): T = syncLock.synchronized { body }
48+
override def isEmpty: Boolean = syncLockSynchronized { super.isEmpty }
49+
override def size: Int = syncLockSynchronized { super.size }
50+
override def enter[T <: Symbol](sym: T): T = syncLockSynchronized { super.enter(sym) }
51+
override def rehash(sym: Symbol, newname: Name) = syncLockSynchronized { super.rehash(sym, newname) }
52+
override def unlink(e: ScopeEntry) = syncLockSynchronized { super.unlink(e) }
53+
override def unlink(sym: Symbol) = syncLockSynchronized { super.unlink(sym) }
54+
override def lookupAll(name: Name) = syncLockSynchronized { super.lookupAll(name) }
55+
override def lookupEntry(name: Name) = syncLockSynchronized { super.lookupEntry(name) }
56+
override def lookupNextEntry(entry: ScopeEntry) = syncLockSynchronized { super.lookupNextEntry(entry) }
57+
override def toList: List[Symbol] = syncLockSynchronized { super.toList }
4958
}
5059
}

0 commit comments

Comments
 (0)