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