mirror of https://github.com/sbt/sbt.git
Add LRUCache to sbt.internal
I am going to add a classloader cache to improve the startup performance of the run and test tasks. To prevent the classloader cache from having unbounded size, I'm adding a simple LRUCache implementation to sbt. An important characteristic of the implementation of the cache is that when entries are evicted, we run a callback to cleanup the entry. This allows us to automatically cleanup any resources created by the entry. This is a pretty naive implementation that uses an array of entries that it manipulates as elements are removed/accessed. In general, I expect these caches to be quite small <= 4 elements, so the storage overhead / performance of the simple implementation should be quite good. If performance ever becomes an issue, we can specialzed LRUCache.apply to use a different implementation for caches with large limits.
This commit is contained in:
parent
e36360977b
commit
ec22d6c0da
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* sbt
|
||||||
|
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||||
|
* Copyright 2008 - 2010, Mark Harrah
|
||||||
|
* Licensed under Apache License 2.0 (see LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sbt.internal
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
|
||||||
|
private[sbt] sealed trait LRUCache[K, V] extends AutoCloseable {
|
||||||
|
def get(key: K): Option[V]
|
||||||
|
def entries: Seq[(K, V)]
|
||||||
|
def maxSize: Int
|
||||||
|
def put(key: K, value: V): Option[V]
|
||||||
|
def remove(key: K): Option[V]
|
||||||
|
def size: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
private[sbt] object LRUCache {
|
||||||
|
private[this] class impl[K, V](override val maxSize: Int, onExpire: Option[((K, V)) => Unit])
|
||||||
|
extends LRUCache[K, V] {
|
||||||
|
private[this] val elementsSortedByAccess: Array[(K, V)] = new Array[(K, V)](maxSize)
|
||||||
|
private[this] val lastIndex: AtomicInteger = new AtomicInteger(-1)
|
||||||
|
|
||||||
|
override def close(): Unit = this.synchronized {
|
||||||
|
val f = onExpire.getOrElse((_: (K, V)) => Unit)
|
||||||
|
0 until maxSize foreach { i =>
|
||||||
|
elementsSortedByAccess(i) match {
|
||||||
|
case null =>
|
||||||
|
case el => f(el)
|
||||||
|
}
|
||||||
|
elementsSortedByAccess(i) = null
|
||||||
|
}
|
||||||
|
lastIndex.set(-1)
|
||||||
|
}
|
||||||
|
override def entries: Seq[(K, V)] = this.synchronized {
|
||||||
|
(0 to lastIndex.get()).map(elementsSortedByAccess)
|
||||||
|
}
|
||||||
|
override def get(key: K): Option[V] = this.synchronized {
|
||||||
|
indexOf(key) match {
|
||||||
|
case -1 => None
|
||||||
|
case i => replace(i, key, elementsSortedByAccess(i)._2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override def put(key: K, value: V): Option[V] = this.synchronized {
|
||||||
|
indexOf(key) match {
|
||||||
|
case -1 =>
|
||||||
|
append(key, value)
|
||||||
|
None
|
||||||
|
case i => replace(i, key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override def remove(key: K): Option[V] = this.synchronized {
|
||||||
|
indexOf(key) match {
|
||||||
|
case -1 => None
|
||||||
|
case i => remove(i, lastIndex.get, expire = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override def size: Int = lastIndex.get + 1
|
||||||
|
override def toString: String = {
|
||||||
|
val values = 0 to lastIndex.get() map { i =>
|
||||||
|
val (key, value) = elementsSortedByAccess(i)
|
||||||
|
s"$key -> $value"
|
||||||
|
}
|
||||||
|
s"LRUCache(${values mkString ", "})"
|
||||||
|
}
|
||||||
|
|
||||||
|
private def indexOf(key: K): Int =
|
||||||
|
elementsSortedByAccess.view.take(lastIndex.get() + 1).indexWhere(_._1 == key)
|
||||||
|
private def replace(index: Int, key: K, value: V): Option[V] = {
|
||||||
|
val prev = remove(index, lastIndex.get(), expire = false)
|
||||||
|
append(key, value)
|
||||||
|
prev
|
||||||
|
}
|
||||||
|
private def append(key: K, value: V): Unit = {
|
||||||
|
while (lastIndex.get() >= maxSize - 1) {
|
||||||
|
remove(0, lastIndex.get(), expire = true)
|
||||||
|
}
|
||||||
|
val index = lastIndex.incrementAndGet()
|
||||||
|
elementsSortedByAccess(index) = (key, value)
|
||||||
|
}
|
||||||
|
private def remove(index: Int, endIndex: Int, expire: Boolean): Option[V] = {
|
||||||
|
@tailrec
|
||||||
|
def shift(i: Int): Unit = if (i < endIndex) {
|
||||||
|
elementsSortedByAccess(i) = elementsSortedByAccess(i + 1)
|
||||||
|
shift(i + 1)
|
||||||
|
}
|
||||||
|
val prev = elementsSortedByAccess(index)
|
||||||
|
shift(index)
|
||||||
|
lastIndex.set(endIndex - 1)
|
||||||
|
if (expire) onExpire.foreach(f => f(prev))
|
||||||
|
Some(prev._2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private def emptyCache[K, V]: LRUCache[K, V] = new LRUCache[K, V] {
|
||||||
|
override def get(key: K): Option[V] = None
|
||||||
|
override def entries: Seq[(K, V)] = Nil
|
||||||
|
override def maxSize: Int = 0
|
||||||
|
override def put(key: K, value: V): Option[V] = None
|
||||||
|
override def remove(key: K): Option[V] = None
|
||||||
|
override def size: Int = 0
|
||||||
|
override def close(): Unit = {}
|
||||||
|
override def toString = "EmptyLRUCache"
|
||||||
|
}
|
||||||
|
def apply[K, V](maxSize: Int): LRUCache[K, V] =
|
||||||
|
if (maxSize > 0) new impl(maxSize, None) else emptyCache
|
||||||
|
def apply[K, V](maxSize: Int, onExpire: (K, V) => Unit): LRUCache[K, V] =
|
||||||
|
if (maxSize > 0) new impl(maxSize, Some(onExpire.tupled)) else emptyCache
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* sbt
|
||||||
|
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||||
|
* Copyright 2008 - 2010, Mark Harrah
|
||||||
|
* Licensed under Apache License 2.0 (see LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sbt
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
import org.scalatest.{ FlatSpec, Matchers }
|
||||||
|
|
||||||
|
class LRUCacheTest extends FlatSpec with Matchers {
|
||||||
|
"LRUCache" should "flush entries when full" in {
|
||||||
|
val cache = LRUCache[Int, Int](2)
|
||||||
|
cache.put(1, 1)
|
||||||
|
cache.put(2, 2)
|
||||||
|
cache.put(3, 3)
|
||||||
|
assert(cache.get(1).isEmpty)
|
||||||
|
assert(cache.get(2).contains(2))
|
||||||
|
assert(cache.get(3).contains(3))
|
||||||
|
|
||||||
|
assert(cache.get(2).contains(2))
|
||||||
|
cache.put(1, 1)
|
||||||
|
assert(cache.get(3).isEmpty)
|
||||||
|
assert(cache.get(2).contains(2))
|
||||||
|
assert(cache.get(1).contains(1))
|
||||||
|
}
|
||||||
|
it should "remove entries" in {
|
||||||
|
val cache = LRUCache[Int, Int](2)
|
||||||
|
cache.put(1, 1)
|
||||||
|
cache.put(2, 2)
|
||||||
|
assert(cache.get(1).contains(1))
|
||||||
|
assert(cache.get(2).contains(2))
|
||||||
|
|
||||||
|
assert(cache.remove(1).getOrElse(-1) == 1)
|
||||||
|
assert(cache.get(1).isEmpty)
|
||||||
|
assert(cache.get(2).contains(2))
|
||||||
|
}
|
||||||
|
it should "clear entries on close" in {
|
||||||
|
val cache = LRUCache[Int, Int](2)
|
||||||
|
cache.put(1, 1)
|
||||||
|
assert(cache.get(1).contains(1))
|
||||||
|
cache.close()
|
||||||
|
assert(cache.get(1).isEmpty)
|
||||||
|
}
|
||||||
|
it should "call onExpire in close" in {
|
||||||
|
val count = new AtomicInteger(0)
|
||||||
|
val cache =
|
||||||
|
LRUCache[Int, Int](
|
||||||
|
maxSize = 3,
|
||||||
|
onExpire = (_: Int, _: Int) => { count.getAndIncrement(); () }
|
||||||
|
)
|
||||||
|
cache.put(1, 1)
|
||||||
|
cache.put(2, 2)
|
||||||
|
cache.put(3, 3)
|
||||||
|
cache.put(4, 4)
|
||||||
|
assert(count.get == 1)
|
||||||
|
cache.close()
|
||||||
|
assert(count.get == 4)
|
||||||
|
}
|
||||||
|
it should "apply on remove function" in {
|
||||||
|
val value = new AtomicInteger(0)
|
||||||
|
val cache = LRUCache[Int, Int](1, (k: Int, v: Int) => value.set(k + v))
|
||||||
|
cache.put(1, 3)
|
||||||
|
cache.put(2, 2)
|
||||||
|
assert(value.get() == 4)
|
||||||
|
assert(cache.get(2).contains(2))
|
||||||
|
}
|
||||||
|
it should "print sorted entries in toString" in {
|
||||||
|
val cache = LRUCache[Int, Int](2)
|
||||||
|
cache.put(2, 2)
|
||||||
|
cache.put(1, 1)
|
||||||
|
assert(cache.toString == s"LRUCache(2 -> 2, 1 -> 1)")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue