Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip on many things #498

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions modules/core/src/main/scala/data/SemispaceCache.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,49 @@ import cats.syntax.all._
*/
sealed abstract case class SemispaceCache[K, V](gen0: Map[K, V], gen1: Map[K, V], max: Int) {

assert(max >= 0)
assert(gen0.size <= max)
assert(gen1.size <= max)
/** All keys in the cache. */
def keySet: Set[K] =
gen0.keySet ++ gen1.keySet

/** Insert the given pair, yielding a new SemispaceCache. */
def insert(k: K, v: V): SemispaceCache[K, V] =
if (max == 0) this // special case, can't insert!
else if (gen0.size < max) SemispaceCache(gen0 + (k -> v), gen1, max) // room in gen0, done!
else SemispaceCache(Map(k -> v), gen0, max) // no room in gen0, slide it down
insertWithEvictions(k, v)._1

/**
* Insert the given pair, yielding a new SemispaceCache and a map containing any entries that
* were evicted as a result.
*/
def insertWithEvictions(k: K, v: V): (SemispaceCache[K, V], Map[K, V]) =
if (max == 0) {
// Special case, can't insert. No evictions.
(this, Map.empty)
} else if (gen0.size < max || gen0.contains(k)) {
// There is room in gen0. Add/replace the mapping. No evictions.
(SemispaceCache(gen0 + (k -> v), gen1, max), Map.empty)
} else {
// There is no room in gen0. Make a new gen0 with a single entry. Our new gen1 is our old
// gen0. Evictions are anything in the old gen1 that isn't in our new
val evicted = gen1 -- (gen0.keySet + k)
(SemispaceCache(Map(k -> v), gen0, max), evicted)
}

/** Look up the given key, yielding a new SemispaceCache and a value on success. */
def lookup(k: K): Option[(SemispaceCache[K, V], V)] =
gen0.get(k).tupleLeft(this) orElse // key is in gen0, done!
gen1.get(k).map(v => (insert(k, v), v)) // key is in gen1, copy to gen0
lookupWithEvictions(k).map { case (c, v, _) => (c, v) }

/**
* Look up the given key, yielding a new SemispaceCache, a value on success, and a map containing
* any entries that were evicted as a result.
*/
def lookupWithEvictions(k: K): Option[(SemispaceCache[K, V], V, Map[K, V])] =
gen0.get(k).map(v => (this, v, Map.empty[K, V])) orElse // key is in gen0, done!
gen1.get(k).map { v =>
// key is in gen1, copy to gen0
val (c, e) = insertWithEvictions(k, v)
(c, v, e)
}

/** True if the given key exists in the cache. */
def containsKey(k: K): Boolean =
gen0.contains(k) || gen1.contains(k)

Expand Down
95 changes: 92 additions & 3 deletions modules/tests/src/test/scala/data/SemispaceCacheTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,117 @@

package skunk.data

import cats.Monad
import cats.data.StateT
import cats.syntax.all._
import munit.ScalaCheckSuite
import org.scalacheck.Gen
import org.scalacheck.Prop._

class SemispaceCacheTest extends ScalaCheckSuite {

implicit val MonadGen: Monad[Gen] =
new Monad[Gen] {
def pure[A](a: A) = Gen.const(a)
def flatMap[A, B](fa: Gen[A])(f: A => Gen[B]) = fa.flatMap(f)
def tailRecM[A, B](a: A)(f: A => Gen[Either[A,B]]): Gen[B] = ???
}

// An empty cache of size in [0..10]
val genEmpty: Gen[SemispaceCache[Int, String]] =
Gen.choose(-1, 10).map(SemispaceCache.empty)

// A short list of ints in [1..10]
val genInts: Gen[List[Int]] =
for {
len <- Gen.choose(1, 25)
list <- Gen.listOfN(len, Gen.choose(1, 10))
} yield list

// Insert or read from `cache`
def update(cache: SemispaceCache[Int, String], key: Int): Gen[SemispaceCache[Int, String]] =
Gen.oneOf(
Gen.const(cache.insert(key, key.toString)),
Gen.const(cache.lookup(key).map(_._1).getOrElse(cache))
)

// Insert and read many times.
def updateMany(cache: SemispaceCache[Int, String], keys: List[Int]): Gen[SemispaceCache[Int, String]] =
keys.traverse(n => StateT.modifyF[Gen, SemispaceCache[Int, String]](update(_, n))).runS(cache)

// A random cache.
def genCache: Gen[SemispaceCache[Int, String]] =
for {
c <- genEmpty
ns <- genInts
cʹ <- updateMany(c, ns)
} yield cʹ

test("max is never negative") {
forAll(genEmpty) { c =>
forAll(genCache) { c =>
assert(c.max >= 0)
}
}

test("insert should allow lookup, unless max == 0") {
forAll(genEmpty) { c =>
test("gen0.size <= max") {
forAll(genCache) { c =>
assert(c.gen1.size <= c.max)
}
}

test("gen1.size <= max") {
forAll(genCache) { c =>
assert(c.gen1.size <= c.max)
}
}

test("insert should allow subsequent lookup, unless max == 0") {
forAll(genCache) { c =>
val cʹ = c.insert(1, "x")
assertEquals(cʹ.lookup(1), if (c.max == 0) None else Some((cʹ, "x")))
}
}

test("all keys in keyset can be looked up") {
forAll(genCache) { c =>
c.keySet.forall { k =>
c.lookup(k).isDefined
}
}
}

test("keys that exist in both generations map to the same value") {
forAll(genCache) { c =>
val intersection = c.gen0.keySet intersect c.gen1.keySet
intersection.forall { k =>
c.gen0(k) == c.gen1(k)
}
}
}

def checkEvictions(c: SemispaceCache[Int, String], cʹ: SemispaceCache[Int, String], es: Map[Int, String]): Unit = {
assert((cʹ.keySet intersect es.keySet).isEmpty, "Keyset and eviction keyset must be disjoint")
c.keySet.foreach { k =>
assert(cʹ.keySet.contains(k) || es.contains(k), "All keys in c must also be in cʹ or es.")
}
}

test("eviction consistency on insert") {
forAll(genCache) { c =>
assert(c.lookup(100).isEmpty)
val (cʹ, es) = c.insertWithEvictions(100, "x")
checkEvictions(c, cʹ, es)
}
}

test("eviction consistency on lookup") {
forAll(genCache, Gen.choose(1, 10)) { (c, k) =>
c.lookupWithEvictions(k).foreach { case (cʹ, _, es) =>
checkEvictions(c, cʹ, es)
}
}
}

test("overflow") {
forAll(genEmpty) { c =>
val max = c.max
Expand Down