Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Zinc - The incremental compiler for Scala.
* Copyright Scala Center, Lightbend, and Mark Harrah
*
* Licensed under Apache License 2.0
* SPDX-License-Identifier: Apache-2.0
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package sbt.internal.inc

import java.util.{ EnumSet => JEnumSet }

import xsbti.UseScope

/**
* The process-wide canonicalizer for strings and `UsedName`s: fresh
* (compiler-produced) and persisted (deserialized) analyses both route through
* it, so co-resident analyses share a single instance per distinct value.
*
* Strings are canonicalized through a weak interner: canonical instances are
* held only through weak references, so once every analysis referencing a
* string is released the canonical instance becomes eligible for GC.
*
* `UsedName`s are canonicalized without constructing a candidate first: a
* `UsedName` is fully determined by its (already canonical) name string and one
* of the eight possible `UseScope` combinations, so each combination gets a
* weak-valued name-keyed pool. A pool entry is removed once the canonical
* `UsedName` is no longer referenced by any analysis. On a pool hit no
* `UsedName` is constructed.
*
* Both pools make interning leak-free across the lifetime of a long-running
* build server. Benchmarks that need an uninterned baseline compare against a
* stock zinc checkout; production code always interns.
*/
object AnalysisInterner {

private[inc] final val DEFAULT_SCOPE = 1
private[inc] final val IMPLICIT_SCOPE = 2
private[inc] final val PAT_MAT_TARGET_SCOPE = 4
private final val SCOPE_COMBINATIONS = 8 // every subset of the three scopes above

private val stringPool = new WeakInterner[String]

// One weak-valued pool per UseScope combination, keyed by the canonical name.
private val usedNamePools: Array[WeakValuePool[String, UsedName]] =
Array.fill(SCOPE_COMBINATIONS)(new WeakValuePool[String, UsedName])

def internString(s: String): String = stringPool.intern(s)

/**
* The canonical `UsedName` for `name` used in the scope combination
* `scopeBits`, an or-ing of `DEFAULT_SCOPE`, `IMPLICIT_SCOPE` and
* `PAT_MAT_TARGET_SCOPE`. Constructs a `UsedName` only when the value is not
* pooled yet.
*/
def usedName(name: String, scopeBits: Int): UsedName = {
// UsedName stores the escaped name, so the pool must be probed with it too;
// escaping is a no-op unless the name contains control characters.
val escaped = UsedName.escapeControlChars(name)
val pool = usedNamePools(scopeBits)
val existing = pool.get(escaped)
if (existing != null) existing
else {
val fresh = UsedName.make(escaped, scopeSets(scopeBits))
val prev = pool.putIfAbsent(fresh.name, fresh)
if (prev == null) fresh else prev
}
}

/**
* The eight possible scope sets, indexed by scope bits. Shared by every
* UsedName and treated as immutable -- they must never be mutated.
*/
private[inc] val scopeSets: Array[JEnumSet[UseScope]] =
Array.tabulate(SCOPE_COMBINATIONS) { bits =>
val scopes = JEnumSet.noneOf(classOf[UseScope])
if ((bits & DEFAULT_SCOPE) != 0) scopes.add(UseScope.Default)
if ((bits & IMPLICIT_SCOPE) != 0) scopes.add(UseScope.Implicit)
if ((bits & PAT_MAT_TARGET_SCOPE) != 0) scopes.add(UseScope.PatMatTarget)
scopes
}

/** The inverse of `scopeSets`: also the encoding used by the persisted format. */
private[inc] def scopeBits(scopes: JEnumSet[UseScope]): Int =
(if (scopes.contains(UseScope.Default)) DEFAULT_SCOPE else 0) |
(if (scopes.contains(UseScope.Implicit)) IMPLICIT_SCOPE else 0) |
(if (scopes.contains(UseScope.PatMatTarget)) PAT_MAT_TARGET_SCOPE else 0)
}
Original file line number Diff line number Diff line change
Expand Up @@ -920,9 +920,18 @@ private final class AnalysisCallback(
}

def usedName(className: String, name: String, useScopes: EnumSet[UseScope]) = {
// Canonicalize freshly-produced used names and their strings into the
// process-wide pools, so a just-compiled analysis shares them with every
// other resident analysis. (Api tree nodes are deduped only when an
// analysis is deserialized, not on this fresh-compile path.)
usedNames
.getOrElseUpdate(className, ConcurrentHashMap.newKeySet[UsedName].asScala)
.add(UsedName.make(name, useScopes))
.add(
AnalysisInterner.usedName(
AnalysisInterner.internString(name),
AnalysisInterner.scopeBits(useScopes)
)
)
()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,23 @@ package sbt.internal.inc

import java.{ util => ju }
import scala.{ collection => sc }
import scala.util.hashing.MurmurHash3
import xsbti.compile.{ UsedName => XUsedName }
import xsbti.UseScope

/**
* `scopes` must never be mutated after construction: instances may share one
* scope set (`make` uses the set it is given, and interning aliases equal
* instances), and the hash below is computed once.
*/
case class UsedName private (name: String, scopes: ju.EnumSet[UseScope]) extends XUsedName {
override def getName: String = name
override def getScopes: ju.EnumSet[UseScope] = scopes

// A canonical (interned) instance is inserted into one name-set per class that
// uses it, and EnumSet.hashCode iterates its elements; caching makes each
// re-hash a field read. Fits in the object's existing alignment padding.
override val hashCode: Int = MurmurHash3.caseClassHash(this)
}

object UsedName {
Expand All @@ -33,7 +44,7 @@ object UsedName {
new UsedName(escapedName, useScopes)
}

private def escapeControlChars(name: String) = {
private[inc] def escapeControlChars(name: String): String = {
if (name.indexOf('\n') > 0) // optimize for common case to regex overhead
name.replace("\n", "\u26680A")
else
Expand Down
117 changes: 117 additions & 0 deletions internal/zinc-core/src/main/scala/sbt/internal/inc/WeakPools.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Zinc - The incremental compiler for Scala.
* Copyright Scala Center, Lightbend, and Mark Harrah
*
* Licensed under Apache License 2.0
* SPDX-License-Identifier: Apache-2.0
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package sbt.internal.inc

import java.lang.ref.{ ReferenceQueue, WeakReference }
import java.util.concurrent.ConcurrentHashMap

import scala.annotation.tailrec

/**
* Canonicalizing pool: `intern` returns one instance per distinct value
* (`equals`/`hashCode`), and holds it only through a weak reference, so a value
* becomes collectable as soon as the last caller drops it. Dead entries are
* expunged by the next pool operation.
*/
private[inc] final class WeakInterner[A <: AnyRef] {
private val stale = new ReferenceQueue[A]
private val pool = new ConcurrentHashMap[WeakValue[A], WeakValue[A]]

def intern(a: A): A = {
expunge()
val candidate = new WeakValue(a, stale)
@tailrec def publish(): A = pool.putIfAbsent(candidate, candidate) match {
case null => a
case existing =>
existing.get match {
case null => // collected since it matched: drop the dead entry and retry
pool.remove(existing, existing)
publish()
case canonical =>
candidate.clear() // never enqueue a reference that was not pooled
canonical
}
}
publish()
}

@tailrec private def expunge(): Unit = stale.poll() match {
case null => ()
case dead =>
pool.remove(dead, dead)
expunge()
}
}

/**
* Map that holds its values only through weak references: an entry disappears
* once its value is unreachable, releasing the strong reference to its key with
* it. Unlike an interner it is keyed by something cheaper than the value, so
* callers can probe the pool before constructing a candidate.
*/
private[inc] final class WeakValuePool[K, V <: AnyRef] {
private val stale = new ReferenceQueue[V]
private val pool = new ConcurrentHashMap[K, KeyedWeakValue[K, V]]

/** The pooled value for `key`, or `null` if there is none. */
def get(key: K): V = {
expunge()
pool.get(key) match {
case null => null.asInstanceOf[V]
case entry => entry.get
}
}

/** Pools `value` under `key`, returning `null`, or the value already pooled. */
@tailrec def putIfAbsent(key: K, value: V): V = {
val candidate = new KeyedWeakValue(key, value, stale)
pool.putIfAbsent(key, candidate) match {
case null => null.asInstanceOf[V]
case existing =>
existing.get match {
case null => // collected since it was published: replace the dead entry
pool.remove(key, existing)
putIfAbsent(key, value)
case pooled =>
candidate.clear() // never enqueue a reference that was not pooled
pooled
}
}
}

@tailrec private def expunge(): Unit = stale.poll() match {
case null => ()
case dead =>
pool.remove(dead.asInstanceOf[KeyedWeakValue[K, V]].key, dead)
expunge()
}
}

/** Weak reference that hashes and compares by the value of its referent. */
private final class WeakValue[A <: AnyRef](a: A, stale: ReferenceQueue[A])
extends WeakReference[A](a, stale) {
private val hash: Int = a.hashCode

override def hashCode(): Int = hash
override def equals(other: Any): Boolean = other match {
case that: WeakValue[?] =>
(this `eq` that) || {
val value = get
value != null && value == that.get
}
case _ => false
}
}

/** Weak reference that remembers the key its value was pooled under. */
private final class KeyedWeakValue[K, V <: AnyRef](val key: K, v: V, stale: ReferenceQueue[V])
extends WeakReference[V](v, stale)
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Zinc - The incremental compiler for Scala.
* Copyright Scala Center, Lightbend, and Mark Harrah
*
* Licensed under Apache License 2.0
* SPDX-License-Identifier: Apache-2.0
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package sbt.internal.inc

import java.lang.ref.WeakReference
import java.util.concurrent.{ Callable, CountDownLatch, Executors, TimeUnit }
import xsbti.UseScope

class AnalysisInternerSpec extends UnitSpec {

behavior of "AnalysisInterner"

it should "canonicalize equal strings to one instance" in {
val s1 = new String("com.example.Foo") // force distinct heap instances
val s2 = new String("com.example.Foo")
assert(s1 `ne` s2)
assert(AnalysisInterner.internString(s1) `eq` AnalysisInterner.internString(s2))
}

it should "return one UsedName instance per (name, scope) without construction on hit" in {
val a = AnalysisInterner.usedName("map", 1)
val b = AnalysisInterner.usedName(new String("map"), 1)
assert(a `eq` b)
assert(a.name == "map")
assert(a.scopes.contains(UseScope.Default))
assert(!a.scopes.contains(UseScope.Implicit))
}

it should "distinguish scope combinations for the same name" in {
val default = AnalysisInterner.usedName("map", 1)
val implicitScope = AnalysisInterner.usedName("map", 2)
val patMat = AnalysisInterner.usedName("map", 4)
assert(default `ne` implicitScope)
assert(default `ne` patMat)
assert(implicitScope.scopes.contains(UseScope.Implicit))
assert(patMat.scopes.contains(UseScope.PatMatTarget))
}

it should "pool names containing control characters under their escaped form" in {
val a = AnalysisInterner.usedName("weird\nname", 1)
val b = AnalysisInterner.usedName("weird\nname", 1)
assert(a `eq` b)
assert(a.name == "weird♨0Aname") // stored escaped, exactly as UsedName.make does
}

it should "converge on one canonical instance under concurrent interning" in {
// Parallel compilation interns from many threads at once. Every thread must
// observe the same canonical reference, and the pools must never deadlock.
val threads = 16
val start = new CountDownLatch(1)
val pool = Executors.newFixedThreadPool(threads)
try {
val futures = (1 to threads).map { _ =>
pool.submit(new Callable[(String, UsedName)] {
def call(): (String, UsedName) = {
start.await() // release all threads together to maximize contention
(
AnalysisInterner.internString(new String("concurrent.value")),
AnalysisInterner.usedName(new String("concurrent.value"), 5)
)
}
})
}
start.countDown()
val results = futures.map(_.get(30, TimeUnit.SECONDS)).toList // deadlock => timeout
assert(results.forall(_._1 `eq` results.head._1))
assert(results.forall(_._2 `eq` results.head._2))
} finally pool.shutdownNow()
}

it should "release canonical instances once no analysis references them" in {
// The leak-free requirement: canonical instances are held only weakly, so
// once every analysis referencing a value is dropped it becomes GC-eligible.
val stringRef =
new WeakReference[String](AnalysisInterner.internString(new String("ephemeral.str")))
val usedNameRef =
new WeakReference[UsedName](AnalysisInterner.usedName("ephemeral.used", 1))
val released = (1 to 100).exists { _ =>
System.gc()
Thread.sleep(10)
stringRef.get() == null && usedNameRef.get() == null
}
assert(released, "weak pools must not retain values after they become unreachable")
}

it should "keep pooled values while an analysis still references them" in {
val held = AnalysisInterner.usedName("held", 1)
System.gc()
Thread.sleep(10)
assert(AnalysisInterner.usedName("held", 1) `eq` held)
}
}
Loading