mirror of https://github.com/sbt/sbt.git
[2.x] Add a dependency-free weak interner to lm-core
WeakInterner is a ConcurrentHashMap keyed by a WeakReference subclass that hashes and compares by its referent, with dead entries expunged on each operation. Ported from zinc's sbt.internal.inc.WeakInterner, so lm-core gains no interning dependency. internWith derives the instance to pool only when the value is not pooled yet. Equality is structural, so a value finds its pooled twin whatever the identity of the values inside it, which lets a caller whose canonical form is itself expensive to build skip building it on a hit. intern is internWith with identity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6698589c59
commit
8cea7c1cbc
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* sbt
|
||||||
|
* Copyright 2023, Scala center
|
||||||
|
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||||
|
* Copyright 2008 - 2010, Mark Harrah
|
||||||
|
* Licensed under Apache License 2.0 (see LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sbt.internal.librarymanagement
|
||||||
|
|
||||||
|
import java.lang.ref.{ ReferenceQueue, WeakReference }
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonicalizing pool: `intern` returns one instance per distinct value, held weakly.
|
||||||
|
*
|
||||||
|
* Ported from zinc's `sbt.internal.inc.WeakInterner` to avoid a new dependency, plus `internWith`.
|
||||||
|
*/
|
||||||
|
private[librarymanagement] 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 = internWith(a)(identity)
|
||||||
|
|
||||||
|
/** Like `intern`, but applies `canonicalize` only on a miss, since equality is structural. */
|
||||||
|
def internWith(a: A)(canonicalize: A => A): A = {
|
||||||
|
expunge()
|
||||||
|
lookup(a) match {
|
||||||
|
case null => publish(canonicalize(a))
|
||||||
|
case hit => hit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The pooled instance value-equal to `a`, or null if there is none. */
|
||||||
|
private def lookup(a: A): A = {
|
||||||
|
val probe = new WeakValue(a, stale)
|
||||||
|
try
|
||||||
|
pool.get(probe) match {
|
||||||
|
case null => null.asInstanceOf[A]
|
||||||
|
case existing => existing.get // null if it was collected since it matched
|
||||||
|
}
|
||||||
|
finally probe.clear() // never enqueue a reference that was not pooled
|
||||||
|
}
|
||||||
|
|
||||||
|
private def publish(a: A): A = {
|
||||||
|
val candidate = new WeakValue(a, stale)
|
||||||
|
@tailrec def attempt(): 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)
|
||||||
|
attempt()
|
||||||
|
case canonical =>
|
||||||
|
candidate.clear()
|
||||||
|
canonical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attempt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailrec private def expunge(): Unit = stale.poll() match {
|
||||||
|
case null => ()
|
||||||
|
case dead =>
|
||||||
|
pool.remove(dead, dead)
|
||||||
|
expunge()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weak reference that hashes and compares by its referent's value.
|
||||||
|
*
|
||||||
|
* The hash is captured eagerly: it must stay stable after the referent is cleared, or the dead entry
|
||||||
|
* could never be found and removed.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* sbt
|
||||||
|
* Copyright 2023, Scala center
|
||||||
|
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||||
|
* Copyright 2008 - 2010, Mark Harrah
|
||||||
|
* Licensed under Apache License 2.0 (see LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sbt.internal.librarymanagement
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.IdentityHashMap
|
||||||
|
import java.util.concurrent.{ ConcurrentLinkedQueue, CountDownLatch }
|
||||||
|
|
||||||
|
object WeakInternerSpec extends verify.BasicTestSuite:
|
||||||
|
|
||||||
|
test("intern returns one instance per distinct value"):
|
||||||
|
val pool = new WeakInterner[Key]
|
||||||
|
val first = Key("shared")
|
||||||
|
assert(pool.intern(first) eq first, "the first instance interned is the canonical one")
|
||||||
|
val second = Key("shared")
|
||||||
|
assert(first ne second)
|
||||||
|
assert(pool.intern(second) eq first, "a value-equal input must return the canonical instance")
|
||||||
|
|
||||||
|
test("interning preserves value equality"):
|
||||||
|
val pool = new WeakInterner[Key]
|
||||||
|
val k = Key("preserved")
|
||||||
|
assert(pool.intern(k) == k)
|
||||||
|
|
||||||
|
test("distinct values get distinct instances"):
|
||||||
|
val pool = new WeakInterner[Key]
|
||||||
|
val a = pool.intern(Key("a"))
|
||||||
|
val b = pool.intern(Key("b"))
|
||||||
|
assert(a ne b)
|
||||||
|
assert(a == Key("a") && b == Key("b"))
|
||||||
|
|
||||||
|
test("an interned value is released once nothing else references it"):
|
||||||
|
// Only a weak reference to the canonical instance is kept, so a strong pool would pin it for the
|
||||||
|
// classloader's life. The pool must let the GC reclaim it.
|
||||||
|
val pool = new WeakInterner[Key]
|
||||||
|
val weak = new WeakReference(pool.intern(Key("released")))
|
||||||
|
assert(awaitCleared(weak), "the pool must not pin a value no caller references")
|
||||||
|
|
||||||
|
test("a collected entry does not block re-interning the same value"):
|
||||||
|
// Exercises the retry path: putIfAbsent matches a dead entry, which has to be dropped and the
|
||||||
|
// fresh candidate published in its place.
|
||||||
|
val pool = new WeakInterner[Key]
|
||||||
|
val weak = new WeakReference(pool.intern(Key("recycled")))
|
||||||
|
assert(awaitCleared(weak))
|
||||||
|
val fresh = Key("recycled")
|
||||||
|
assert(pool.intern(fresh) eq fresh, "a dead entry must be replaced, not returned")
|
||||||
|
|
||||||
|
test("internWith derives the pooled instance only when the value is not pooled yet"):
|
||||||
|
val pool = new WeakInterner[Key]
|
||||||
|
var derived = 0
|
||||||
|
val canonicalize: Key => Key = k =>
|
||||||
|
derived += 1
|
||||||
|
k
|
||||||
|
val first = pool.internWith(Key("derived"))(canonicalize)
|
||||||
|
assert(derived == 1, "the first intern has to derive what it pools")
|
||||||
|
val second = pool.internWith(Key("derived"))(canonicalize)
|
||||||
|
assert(second eq first)
|
||||||
|
assert(derived == 1, "a hit must not derive a value it would only discard")
|
||||||
|
|
||||||
|
test("internWith pools what canonicalize returns, not what it was given"):
|
||||||
|
// Deriving is only worth having because the pooled instance differs from the argument -- for a
|
||||||
|
// module report, by holding interned children. Probing by value still has to find it.
|
||||||
|
val pool = new WeakInterner[Key]
|
||||||
|
val canonical = Key("canonical")
|
||||||
|
val pooled = pool.internWith(Key("canonical"))(_ => canonical)
|
||||||
|
assert(pooled eq canonical)
|
||||||
|
val later = pool.intern(Key("canonical"))
|
||||||
|
assert(later eq canonical, "what canonicalize returned is the canonical instance from then on")
|
||||||
|
|
||||||
|
test("an entry survives the collection of a value that probed for it"):
|
||||||
|
// A miss probes with one instance and pools another, value-equal one, so the two hash to the same
|
||||||
|
// bucket. Once the probe is collected, expunging it must not take the live entry with it.
|
||||||
|
val pool = new WeakInterner[Key]
|
||||||
|
val pooled = pool.internWith(Key("probed"))(_ => Key("probed"))
|
||||||
|
// The probe is already unreachable; this waits for a GC to have run, which is what would enqueue
|
||||||
|
// it. The canary is built outside the assert because the assert macro records -- and so retains --
|
||||||
|
// every intermediate value it evaluates.
|
||||||
|
val canary = new WeakReference(Key("collectable"))
|
||||||
|
assert(awaitCleared(canary))
|
||||||
|
val again = pool.intern(Key("probed"))
|
||||||
|
assert(again eq pooled, "expunge must not evict a live entry a dead probe hashes to")
|
||||||
|
|
||||||
|
test("concurrent interning of one value converges on a single instance"):
|
||||||
|
// The parallel per-project update tasks hit these pools at once, so publication has to be atomic.
|
||||||
|
val pool = new WeakInterner[Key]
|
||||||
|
val results = new ConcurrentLinkedQueue[Key]
|
||||||
|
val start = new CountDownLatch(1)
|
||||||
|
val workers = (1 to 8).map: _ =>
|
||||||
|
val t = new Thread(() =>
|
||||||
|
start.await()
|
||||||
|
var i = 0
|
||||||
|
while i < 200 do
|
||||||
|
results.add(pool.intern(Key("contended")))
|
||||||
|
i += 1
|
||||||
|
)
|
||||||
|
t.start()
|
||||||
|
t
|
||||||
|
start.countDown()
|
||||||
|
workers.foreach(_.join())
|
||||||
|
val distinct = new IdentityHashMap[Key, Boolean]
|
||||||
|
results.forEach(k => distinct.put(k, true))
|
||||||
|
assert(distinct.size == 1, s"expected one canonical instance, got ${distinct.size}")
|
||||||
|
|
||||||
|
private case class Key(name: String)
|
||||||
|
|
||||||
|
// Weak references are cleared by the GC, which is only advisory, so retry a bounded number of times
|
||||||
|
// rather than relying on a single System.gc().
|
||||||
|
private def awaitCleared(ref: WeakReference[?]): Boolean =
|
||||||
|
var i = 0
|
||||||
|
while i < 50 && ref.get != null do
|
||||||
|
System.gc()
|
||||||
|
Thread.sleep(20)
|
||||||
|
i += 1
|
||||||
|
ref.get == null
|
||||||
Loading…
Reference in New Issue