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