diff --git a/core/src/main/java/eu/ostrzyciel/jelly/core/NewEncoderLookup.java b/core/src/main/java/eu/ostrzyciel/jelly/core/NewEncoderLookup.java new file mode 100644 index 00000000..de25eea9 --- /dev/null +++ b/core/src/main/java/eu/ostrzyciel/jelly/core/NewEncoderLookup.java @@ -0,0 +1,110 @@ +package eu.ostrzyciel.jelly.core; + +import java.util.HashMap; + +public class NewEncoderLookup { + public final static class LookupEntry { + public int getId; + public int setId; + public boolean newEntry; + public int serial = 1; + + public LookupEntry(int getId, int setId) { + this.getId = getId; + this.setId = setId; + } + + public LookupEntry(int getId, int setId, boolean newEntry) { + this.getId = getId; + this.setId = setId; + this.newEntry = newEntry; + } + } + + HashMap map = new HashMap<>(); + // Layout: [left, right, serial] + // Head: table[1] + int[] table; + int tail; + final int size; + int used; + int lastSetId; + String[] names; + + LookupEntry entryForReturns = new LookupEntry(0, 0, true); + + public NewEncoderLookup(int size) { + this.size = size; + table = new int[(size + 1) * 3]; + names = new String[size + 1]; + } + + public void onAccess(int id) { + int base = id * 3; + if (base == tail) { + return; + } + int left = table[base]; + int right = table[base + 1]; + // Set left's right to our right + table[left + 1] = right; + // Set right's left to our left + table[right] = left; + // Set our left to the tail + table[base] = tail; + // Set the tail's right to us + table[tail + 1] = base; + // Update the tail + tail = base; + } + + public LookupEntry addEntry(String key) { + var value = map.get(key); + if (value != null) { + onAccess(value.getId); + return value; + } + + int id; + if (used < size) { + id = ++used; + int base = id * 3; + // Set the left to the tail + table[base] = tail; + // Right is already 0 + // table[base + 1] = 0; + // Serial is zero, set it to 0+1 = 1 + table[base + 2] = 1; + // Set the tail's right to us + table[tail + 1] = base; + tail = base; + names[id] = key; + map.put(key, new LookupEntry(id, id)); + // setId is 0 because we are adding a new entry sequentially + entryForReturns.setId = 0; + // .serial is already 1 by default + // entryForReturns.serial = 1; + } else { + int base = table[1]; + // Evict the least recently used + id = base / 3; + // Remove the entry from the map + LookupEntry oldEntry = map.remove(names[id]); + oldEntry.getId = id; + oldEntry.setId = id; + int serial = table[base + 2] + 1; + oldEntry.serial = serial; + table[base + 2] = serial; + // Insert the new entry + names[id] = key; + map.put(key, oldEntry); + // Update the table + onAccess(id); + entryForReturns.serial = serial; + entryForReturns.setId = lastSetId + 1 == id ? 0 : id; + } + entryForReturns.getId = id; + lastSetId = id; + return entryForReturns; + } +} diff --git a/core/src/test/scala/eu/ostrzyciel/jelly/core/EncoderLookupSpec.scala b/core/src/test/scala/eu/ostrzyciel/jelly/core/EncoderLookupSpec.scala new file mode 100644 index 00000000..6a223e07 --- /dev/null +++ b/core/src/test/scala/eu/ostrzyciel/jelly/core/EncoderLookupSpec.scala @@ -0,0 +1,128 @@ +package eu.ostrzyciel.jelly.core + +import org.scalatest.Inspectors +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import scala.util.Random + +class EncoderLookupSpec extends AnyWordSpec, Matchers: + Random.setSeed(123) + + "encoder lookup" should { + "add new entries up to capacity" in { + val lookup = NewEncoderLookup(4) + for i <- 1 to 4 do + val v = lookup.addEntry(s"v$i") + v.getId should be (i) + v.setId should be (0) + v.newEntry should be (true) + v.serial should be (1) + } + + "retrieve entries" in { + val lookup = NewEncoderLookup(4) + for i <- 1 to 4 do + lookup.addEntry(s"v$i") + for i <- 1 to 4 do + val v = lookup.addEntry(s"v$i") + v.getId should be (i) + v.setId should be (i) + v.newEntry should be (false) + v.serial should be (1) + } + + "retrieve entries many times, in random order" in { + val lookup = NewEncoderLookup(50) + for i <- 1 to 50 do + lookup.addEntry(s"v$i") + for _ <- 1 to 20 do + for i <- Random.shuffle(1 to 50) do + val v = lookup.addEntry(s"v$i") + v.getId should be (i) + v.setId should be (i) + v.newEntry should be (false) + v.serial should be (1) + } + + "overwrite existing entries, from oldest to newest" in { + val lookup = NewEncoderLookup(4) + for i <- 1 to 4 do + lookup.addEntry(s"v$i") + + val v = lookup.addEntry("v5") + v.getId should be (1) + v.setId should be (1) + v.newEntry should be (true) + v.serial should be (2) + + for i <- 6 to 8 do + val v = lookup.addEntry(s"v$i") + v.getId should be (i - 4) + v.setId should be (0) + v.newEntry should be (true) + v.serial should be (2) + } + + "overwrite existing entries in order, many times" in { + val lookup = NewEncoderLookup(17) + for i <- 1 to 17 do + lookup.addEntry(s"v$i") + + for k <- 2 to 23 do + val v = lookup.addEntry(s"v1 $k") + v.getId should be (1) + v.setId should be (1) + v.newEntry should be (true) + v.serial should be (k) + for i <- 2 to 17 do + val v = lookup.addEntry(s"v$i $k") + v.getId should be (i) + v.setId should be (0) + v.newEntry should be (true) + v.serial should be (k) + } + + "pass random stress test (1)" in { + val lookup = NewEncoderLookup(100) + val frequentSet = (1 to 10).map(i => s"v$i") + frequentSet.foreach(lookup.addEntry) + + for i <- 1 to 50 do + for fIndex <- 1 to 10 do + val v = lookup.addEntry(frequentSet(fIndex - 1)) + v.getId should be (fIndex) + v.setId should be (fIndex) + v.newEntry should be (false) + v.serial should be (1) + + for _ <- 1 to 80 do + val v = lookup.addEntry(s"r${Random.nextInt(200) + 1}") + v.getId should be > 10 + if v.setId != 0 then + v.setId should be > 10 + } + + "pass random stress test (2)" in { + val lookup = NewEncoderLookup(113) + for i <- 1 to 20 do + lookup.addEntry(s"v$i") + for _ <- 1 to 1000 do + val id = Random.nextInt(20) + 1 + val v = lookup.addEntry(s"v$id") + v.getId should be (id) + if v.setId != 0 then + v.setId should be (id) + v.newEntry should be (false) + else + v.newEntry should be (true) + v.serial should be (1) + } + + "pass random stress test (3)" in { + val lookup = NewEncoderLookup(1023) + for _ <- 1 to 100_000 do + val v = lookup.addEntry(s"v${Random.nextInt(10_000) + 1}") + v.getId should be > 0 + } + }