From ec22d6c0da5fe9a0264b92bb91d0c88016310b62 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Sat, 24 Nov 2018 10:57:34 -0800 Subject: [PATCH] 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. --- .../main/scala/sbt/internal/LRUCache.scala | 113 ++++++++++++++++++ .../scala/sbt/internal/LRUCacheTest.scala | 78 ++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 main/src/main/scala/sbt/internal/LRUCache.scala create mode 100644 main/src/test/scala/sbt/internal/LRUCacheTest.scala diff --git a/main/src/main/scala/sbt/internal/LRUCache.scala b/main/src/main/scala/sbt/internal/LRUCache.scala new file mode 100644 index 000000000..a80a2837a --- /dev/null +++ b/main/src/main/scala/sbt/internal/LRUCache.scala @@ -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 +} diff --git a/main/src/test/scala/sbt/internal/LRUCacheTest.scala b/main/src/test/scala/sbt/internal/LRUCacheTest.scala new file mode 100644 index 000000000..06add1271 --- /dev/null +++ b/main/src/test/scala/sbt/internal/LRUCacheTest.scala @@ -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)") + } +}