Skip to content

Commit

Permalink
Add fast file and in memory cache implementations (#422)
Browse files Browse the repository at this point in the history
* Add lightweight read only cache implementation with array backing

* Add more efficient decompression

* Add fast random access file cache impl

* Ad script to remove bzip2 compression from a cache

* Add cache parallel loading

* Add script to remove xteas

* Add automated script to build a new cache

* Fix equipment stats attack rate and examines closes #154

* Move Xteas and binary file into tools module

* Remove ActiveCache.kt

* Rename decoder loadCache() back to load()

* Fix client script and interface encoding

* Add middle mouse camera panning cs2 and interface events closes #418

* Update prefetch keys

* Fix charge spell unit tests

* Add script to check file list against existing hashes
  • Loading branch information
GregHib authored Jan 15, 2024
1 parent e08e897 commit 06d1e20
Show file tree
Hide file tree
Showing 150 changed files with 3,013 additions and 1,642 deletions.
2 changes: 2 additions & 0 deletions cache/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ dependencies {
implementation("com.displee:rs-cache-library:${findProperty("displeeCacheVersion")}")
implementation("io.insert-koin:koin-core:${findProperty("koinVersion")}")

implementation("com.github.jponge:lzma-java:1.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${findProperty("kotlinCoroutinesVersion")}")
implementation("it.unimi.dsi:fastutil:${findProperty("fastUtilVersion")}")
implementation("ch.qos.logback:logback-classic:${findProperty("logbackVersion")}")
implementation("com.michael-bull.kotlin-inline-logger:kotlin-inline-logger-jvm:${findProperty("inlineLoggingVersion")}")
Expand Down
26 changes: 15 additions & 11 deletions cache/src/main/kotlin/world/gregs/voidps/cache/Cache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,36 @@ package world.gregs.voidps.cache

interface Cache {

fun getFile(index: Int, archive: Int, file: Int = 0, xtea: IntArray? = null): ByteArray?
fun indexCount(): Int

fun getFile(index: Int, name: String, xtea: IntArray? = null): ByteArray?
fun indices(): IntArray

fun close()
fun archives(index: Int): IntArray

fun getIndexCrc(indexId: Int): Int
fun archiveCount(index: Int): Int

fun archiveCount(indexId: Int, archiveId: Int): Int
fun lastArchiveId(indexId: Int): Int

fun lastFileId(indexId: Int, archive: Int): Int
fun archiveId(index: Int, hash: Int): Int

fun lastArchiveId(indexId: Int): Int
fun archiveId(index: Int, name: String): Int = archiveId(index, name.hashCode())

fun getArchiveId(index: Int, name: String): Int
fun files(index: Int, archive: Int): IntArray

fun getArchiveId(index: Int, archive: Int): Int
fun fileCount(indexId: Int, archiveId: Int): Int

fun getArchives(index: Int): IntArray
fun lastFileId(indexId: Int, archive: Int): Int

fun data(index: Int, archive: Int, file: Int = 0, xtea: IntArray? = null): ByteArray?

fun data(index: Int, name: String, xtea: IntArray? = null) = data(index, archiveId(index, name), xtea = xtea)

fun write(index: Int, archive: Int, file: Int, data: ByteArray, xteas: IntArray? = null)

fun write(index: Int, archive: String, data: ByteArray, xteas: IntArray? = null)

fun update(): Boolean

fun getArchiveData(index: Int, archive: Int): Map<Int, ByteArray?>?
fun close()

}
70 changes: 29 additions & 41 deletions cache/src/main/kotlin/world/gregs/voidps/cache/CacheDelegate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,66 @@ package world.gregs.voidps.cache

import com.displee.cache.CacheLibrary
import com.github.michaelbull.logging.InlineLogger
import java.lang.ref.Reference
import java.lang.ref.SoftReference

class CacheDelegate(directory: String) : Cache {

private val delegate: Reference<CacheLibrary>

private val logger = InlineLogger()
private val library: CacheLibrary

init {
val start = System.currentTimeMillis()
delegate = SoftReference(CacheLibrary(directory))
library = CacheLibrary(directory)
logger.info { "Cache read from $directory in ${System.currentTimeMillis() - start}ms" }
}

override fun getFile(index: Int, archive: Int, file: Int, xtea: IntArray?) =
delegate.get()?.data(index, archive, file, xtea)

override fun getFile(index: Int, name: String, xtea: IntArray?) = delegate.get()?.data(index, name, xtea)

override fun close() {
delegate.get()?.close()
delegate.clear()
}
override fun indexCount() = library.indices().size

override fun getIndexCrc(indexId: Int): Int {
return delegate.get()?.index(indexId)?.crc ?: 0
}
override fun indices() = library.indices().map { it.id }.toIntArray()

override fun archiveCount(indexId: Int, archiveId: Int): Int {
return delegate.get()?.index(indexId)?.archive(archiveId)?.fileIds()?.size ?: 0
}
override fun archives(index: Int) = library.index(index).archiveIds()

override fun lastFileId(indexId: Int, archive: Int): Int {
return delegate.get()?.index(indexId)?.archive(archive)?.last()?.id ?: -1
}
override fun archiveCount(index: Int) = library.index(index).archiveIds().size

override fun lastArchiveId(indexId: Int): Int {
return delegate.get()?.index(indexId)?.last()?.id ?: 0
}
override fun lastArchiveId(indexId: Int) = library.index(indexId).last()?.id ?: -1

override fun getArchiveId(index: Int, name: String): Int {
return delegate.get()?.index(index)?.archiveId(name) ?: -1
}
override fun archiveId(index: Int, name: String) = library.index(index).archiveId(name)

override fun getArchiveId(index: Int, hash: Int): Int {
delegate.get()?.index(index)?.archives()?.forEach { archive ->
override fun archiveId(index: Int, hash: Int): Int {
for (archive in library.index(index).archives()) {
if (archive.hashName == hash) {
return archive.id
}
}
return -1
}

override fun getArchiveData(index: Int, archive: Int): Map<Int, ByteArray?>? {
return delegate.get()?.index(index)?.archive(archive)?.files?.mapValues { it.value?.data }
}
override fun files(index: Int, archive: Int) = library.index(index).archive(archive)?.fileIds() ?: IntArray(0)

override fun getArchives(index: Int): IntArray {
return delegate.get()?.index(index)?.archiveIds() ?: intArrayOf()
}
override fun fileCount(indexId: Int, archiveId: Int) = library.index(indexId).archive(archiveId)?.fileIds()?.size ?: 0

override fun lastFileId(indexId: Int, archive: Int) = library.index(indexId).archive(archive)?.last()?.id ?: -1

override fun data(index: Int, archive: Int, file: Int, xtea: IntArray?) = library.data(index, archive, file, xtea)

override fun data(index: Int, name: String, xtea: IntArray?) = library.data(index, name, xtea)

override fun write(index: Int, archive: Int, file: Int, data: ByteArray, xteas: IntArray?) {
delegate.get()?.put(index, archive, file, data, xteas)
library.put(index, archive, file, data, xteas)
}

override fun write(index: Int, archive: String, data: ByteArray, xteas: IntArray?) {
delegate.get()?.put(index, archive, data, xteas)
library.put(index, archive, data, xteas)
}

override fun update(): Boolean {
delegate.get()?.update()
library.update()
return true
}

override fun close() {
library.close()
}

companion object {
private val logger = InlineLogger()
}
}
25 changes: 25 additions & 0 deletions cache/src/main/kotlin/world/gregs/voidps/cache/CacheLoader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package world.gregs.voidps.cache

import java.io.File
import java.io.FileNotFoundException
import java.io.RandomAccessFile

interface CacheLoader {

fun load(path: String, xteas: Map<Int, IntArray>? = null, threadUsage: Double = 1.0): Cache {
val mainFile = File(path, "${FileCache.CACHE_FILE_NAME}.dat2")
if (!mainFile.exists()) {
throw FileNotFoundException("Main file not found at '${mainFile.absolutePath}'.")
}
val main = RandomAccessFile(mainFile, "r")
val index255File = File(path, "${FileCache.CACHE_FILE_NAME}.idx255")
if (!index255File.exists()) {
throw FileNotFoundException("Checksum file not found at '${index255File.absolutePath}'.")
}
val index255 = RandomAccessFile(index255File, "r")
val indexCount = index255.length().toInt() / ReadOnlyCache.INDEX_SIZE
return load(path, mainFile, main, index255File, index255, indexCount, xteas, threadUsage)
}

fun load(path: String, mainFile: File, main: RandomAccessFile, index255File: File, index255: RandomAccessFile, indexCount: Int, xteas: Map<Int, IntArray>? = null, threadUsage: Double = 1.0): Cache
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,12 @@ package world.gregs.voidps.cache
import com.github.michaelbull.logging.InlineLogger
import world.gregs.voidps.buffer.read.BufferReader
import world.gregs.voidps.buffer.read.Reader
import world.gregs.voidps.cache.active.ActiveCache
import java.io.File
import java.nio.BufferUnderflowException

abstract class DefinitionDecoder<T : Definition>(val index: Int) {

abstract fun create(size: Int): Array<T>

/**
* Load from active cache
*/
fun load(cache: File): Array<T> {
val start = System.currentTimeMillis()
val file = cache.resolve(fileName())
if (!file.exists()) {
return create(0)
}
val reader = BufferReader(file.readBytes())
val size = reader.readInt() + 1
val array = create(size)
while (reader.position() < reader.length) {
load(array, reader)
}
logger.info { "$size ${this::class.simpleName} definitions loaded in ${System.currentTimeMillis() - start}ms" }
return array
}

open fun fileName() = ActiveCache.indexFile(index)

open fun load(definitions: Array<T>, reader: Reader) {
val id = readId(reader)
read(definitions, id, reader)
Expand All @@ -42,7 +19,7 @@ abstract class DefinitionDecoder<T : Definition>(val index: Int) {
/**
* Load from cache
*/
open fun loadCache(cache: Cache): Array<T> {
open fun load(cache: Cache): Array<T> {
val start = System.currentTimeMillis()
val size = size(cache) + 1
val definitions = create(size)
Expand All @@ -59,13 +36,13 @@ abstract class DefinitionDecoder<T : Definition>(val index: Int) {
}

open fun size(cache: Cache): Int {
return cache.lastArchiveId(index) * 256 + (cache.archiveCount(index, cache.lastArchiveId(index)))
return cache.lastArchiveId(index) * 256 + (cache.fileCount(index, cache.lastArchiveId(index)))
}

open fun load(definitions: Array<T>, cache: Cache, id: Int) {
val archive = getArchive(id)
val file = getFile(id)
val data = cache.getFile(index, archive, file) ?: return
val data = cache.data(index, archive, file) ?: return
read(definitions, id, BufferReader(data))
}

Expand Down
79 changes: 79 additions & 0 deletions cache/src/main/kotlin/world/gregs/voidps/cache/FileCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package world.gregs.voidps.cache

import world.gregs.voidps.cache.compress.DecompressionContext
import java.io.File
import java.io.RandomAccessFile

/**
* [Cache] which reads data directly from file
* Average read speeds, fast loading and low but variable memory usage.
*/
class FileCache(
private val main: RandomAccessFile,
private val indexes: Array<RandomAccessFile?>,
indexCount: Int,
val xteas: Map<Int, IntArray>?
) : ReadOnlyCache(indexCount) {

private val dataCache = object : LinkedHashMap<Int, Array<ByteArray?>>(16, 0.75f, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, Array<ByteArray?>>?): Boolean {
return size > 12
}
}
private val length = main.length()
private val context = DecompressionContext()

override fun data(index: Int, archive: Int, file: Int, xtea: IntArray?): ByteArray? {
val matchingIndex = files.getOrNull(index)?.getOrNull(archive)?.indexOf(file) ?: -1
if (matchingIndex == -1) {
return null
}
val hash = index + (archive shl 6)
val files = dataCache.getOrPut(hash) {
val indexRaf = indexes[index] ?: return null
readFileData(context, main, length, indexRaf, index, archive, xteas) ?: return null
}
return files[matchingIndex]
}

override fun close() {
main.close()
for (file in indexes) {
file?.close()
}
}

companion object : CacheLoader {
const val CACHE_FILE_NAME = "main_file_cache"

operator fun invoke(path: String, xteas: Map<Int, IntArray>? = null): Cache {
return load(path, xteas)
}

/**
* Create [RandomAccessFile]'s for each index file, load only the archive data into memory
*/
override fun load(path: String, mainFile: File, main: RandomAccessFile, index255File: File, index255: RandomAccessFile, indexCount: Int, xteas: Map<Int, IntArray>?, threadUsage: Double): Cache {
val length = mainFile.length()
val context = DecompressionContext()
val indices = Array(indexCount) { indexId ->
val file = File(path, "${CACHE_FILE_NAME}.idx$indexId")
if (file.exists()) RandomAccessFile(file, "r") else null
}
val cache = FileCache(main, indices, indexCount, xteas)
for (indexId in 0 until indexCount) {
cache.readArchiveData(context, main, length, index255, indexId)
}
return cache
}

@JvmStatic
fun main(args: Array<String>) {
val path = "./data/cache/"

val start = System.currentTimeMillis()
val cache = load(path)
println("Loaded cache in ${System.currentTimeMillis() - start}ms")
}
}
}
Loading

0 comments on commit 06d1e20

Please sign in to comment.