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:
Ethan Atkins 2018-11-24 10:57:34 -08:00
parent e36360977b
commit ec22d6c0da
2 changed files with 191 additions and 0 deletions

View File

@ -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
}

View File

@ -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)")
}
}