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

Add an LRU Cache for stored items #385

Merged
merged 7 commits into from
Jan 13, 2024
Merged
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
12 changes: 12 additions & 0 deletions src/main/java/it/smartphonecombo/uecapabilityparser/cli/Clikt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ object Server : CliktCommand(name = "server", help = "Starts ue capability parse
option("--reparse", help = HelpMessage.REPARSE, metavar = "Strategy")
.choice("off", "auto", "force")
.default("off")
val cache by
option("--cache", metavar = "Items", help = HelpMessage.LIBRARY_CACHE)
.int()
.default(1000)
}

private const val DEFAULT_PORT = 0
Expand Down Expand Up @@ -267,6 +271,7 @@ object Server : CliktCommand(name = "server", help = "Starts ue capability parse
Config["store"] = it.path
Config["compression"] = it.compression.toString()
Config["reparse"] = it.reparse
Config["cache"] = it.cache.toString()
}

// Start server
Expand All @@ -293,6 +298,13 @@ object Server : CliktCommand(name = "server", help = "Starts ue capability parse
if (Config.getOrDefault("reparse", "off") != "off")
features += "reparse ${Config["reparse"]}"

features +=
when (val items = Config["cache"]?.toIntOrNull()?.takeIf { it >= 0 }) {
0 -> "cache disabled"
null -> "cache unlimited"
else -> "cache $items items"
}

val featuresString =
if (features.isNotEmpty()) {
features.joinToString(" + ", " with ")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@ object HelpMessage {
const val NEW_CSV_FORMAT = "Use new CSV format for LTE CA"
const val CUSTOM_CSS = "Inject custom css in Web UI"
const val CUSTOM_JS = "Inject custom js in Web UI"
const val LIBRARY_CACHE =
"Number of items to cache, each items occupies ~0.15MB of RAM. 0 = disabled, -1 = unlimited."
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ internal fun BwMap.merge(other: BwMap) {
this[key] = this[key]?.plus(value) ?: value
}
}

internal fun <T> List<T>.trimToSize() {
if (this is ArrayList) {
this.trimToSize()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import it.smartphonecombo.uecapabilityparser.extension.custom
import it.smartphonecombo.uecapabilityparser.extension.decodeFromInputSource
import it.smartphonecombo.uecapabilityparser.extension.nameWithoutAnyExtension
import it.smartphonecombo.uecapabilityparser.extension.toInputSource
import it.smartphonecombo.uecapabilityparser.io.IOUtils
import it.smartphonecombo.uecapabilityparser.io.IOUtils.createDirectories
import it.smartphonecombo.uecapabilityparser.io.IOUtils.echoSafe
import it.smartphonecombo.uecapabilityparser.model.Capabilities
import it.smartphonecombo.uecapabilityparser.util.LruCache
import it.smartphonecombo.uecapabilityparser.util.optimize
import java.io.File
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
Expand All @@ -14,9 +18,11 @@ import kotlinx.serialization.json.Json
@Serializable
data class LibraryIndex(
private val items: MutableList<IndexLine>,
private val multiItems: MutableList<MultiIndexLine> = mutableListOf()
private val multiItems: MutableList<MultiIndexLine> = mutableListOf(),
@Transient private val outputCacheSize: Int? = 0
) {
@Transient private val lock = Any()
@Transient private val outputCache = LruCache<String, Capabilities>(outputCacheSize)

fun addLine(line: IndexLine): Boolean {
synchronized(lock) {
Expand All @@ -42,13 +48,24 @@ data class LibraryIndex(
return items.find { item -> item.inputs.any { it == id } }
}

fun findByOutput(id: String): IndexLine? = find(id)

/** return a list of all single-capability indexes */
fun getAll() = items.toList()

fun getOutput(id: String, libraryPath: String): Capabilities? {
val cached = outputCache[id]
if (cached != null) return cached

val indexLine = find(id) ?: return null
val compressed = indexLine.compressed
val filePath = "$libraryPath/output/$id.json"
val text = IOUtils.getInputSource(filePath, compressed) ?: return null
val res = Json.custom().decodeFromInputSource<Capabilities>(text)
if (outputCache.put(id, res)) res.optimize()
return res
}

companion object {
fun buildIndex(path: String): LibraryIndex {
fun buildIndex(path: String, outputCacheSize: Int?): LibraryIndex {
val outputDir = "$path/output"
val inputDir = "$path/input"
val multiDir = "$path/multi"
Expand Down Expand Up @@ -108,7 +125,7 @@ data class LibraryIndex(
items.sortBy { it.timestamp }
multiItems.sortBy { it.timestamp }

return LibraryIndex(items, multiItems)
return LibraryIndex(items, multiItems, outputCacheSize)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,18 @@ class JavalinApp {
init {
val store = Config["store"]
val compression = Config["compression"] == "true"
val maxOutputCache = Config.getOrDefault("cache", "0").toInt().takeIf { it >= 0 }
var index: LibraryIndex =
store?.let { LibraryIndex.buildIndex(it) } ?: LibraryIndex(mutableListOf())
store?.let { LibraryIndex.buildIndex(it, maxOutputCache) }
?: LibraryIndex(mutableListOf())
val idRegex = "[a-f0-9-]{36}(?:-[0-9]+)?".toRegex()

val reparseStrategy = Config.getOrDefault("reparse", "off")
if (store != null && reparseStrategy != "off") {
CoroutineScope(Dispatchers.IO).launch {
reparseLibrary(reparseStrategy, store, index, compression)
// Rebuild index
index = LibraryIndex.buildIndex(store)
index = LibraryIndex.buildIndex(store, maxOutputCache)
}
}

Expand Down Expand Up @@ -210,15 +212,9 @@ class JavalinApp {
return@apiBuilderGet ctx.badRequest()
}

val indexLine = index.findByOutput(id) ?: return@apiBuilderGet ctx.notFound()
val compressed = indexLine.compressed
val filePath = "$store/output/$id.json"

try {
val text =
IOUtils.getInputSource(filePath, compressed)
?: return@apiBuilderGet ctx.notFound()
val capabilities = Json.custom().decodeFromInputSource<Capabilities>(text)
val capabilities =
index.getOutput(id, store) ?: return@apiBuilderGet ctx.notFound()
ctx.json(capabilities)
} catch (ex: Exception) {
ctx.internalError()
Expand All @@ -235,15 +231,7 @@ class JavalinApp {
val capabilitiesList = mutableListWithCapacity<Capabilities>(indexLineIds.size)
try {
for (indexId in indexLineIds) {
val indexLine = index.find(indexId) ?: continue
val compressed = indexLine.compressed
val outputId = indexLine.id
val filePath = "$store/output/$outputId.json"
val text =
IOUtils.getInputSource(filePath, compressed)
?: return@apiBuilderGet ctx.notFound()
val capabilities =
Json.custom().decodeFromInputSource<Capabilities>(text)
val capabilities = index.getOutput(indexId, store) ?: continue
capabilitiesList.add(capabilities)
}
} catch (ex: Exception) {
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/it/smartphonecombo/uecapabilityparser/util/Intern.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package it.smartphonecombo.uecapabilityparser.util

import it.smartphonecombo.uecapabilityparser.model.EmptyMimo
import it.smartphonecombo.uecapabilityparser.model.Mimo
import it.smartphonecombo.uecapabilityparser.model.bandwidth.Bandwidth
import it.smartphonecombo.uecapabilityparser.model.bandwidth.EmptyBandwidth
import it.smartphonecombo.uecapabilityparser.model.modulation.EmptyModulation
import it.smartphonecombo.uecapabilityparser.model.modulation.Modulation

open class InternMap<T>(maxCapacity: Int) {
private val internalMap: LinkedHashMap<T, T> =
object : LinkedHashMap<T, T>(minOf(16, maxCapacity), 0.75f) {
override fun removeEldestEntry(eldest: Map.Entry<T, T>): Boolean {
return size > maxCapacity
}
}

private fun put(value: T): T {
internalMap[value] = value
return value
}

fun intern(value: T): T = internalMap[value] ?: put(value)
}

object MimoInternMap : InternMap<Mimo>(100)

object ModulationInternMap : InternMap<Modulation>(100)

object BandwidthInternMap : InternMap<Bandwidth>(100)

internal fun Mimo.intern(): Mimo = if (this == EmptyMimo) this else MimoInternMap.intern(this)

internal fun Modulation.intern() =
if (this == EmptyModulation) this else ModulationInternMap.intern(this)

internal fun Bandwidth.intern() =
if (this == EmptyBandwidth) this else BandwidthInternMap.intern(this)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package it.smartphonecombo.uecapabilityparser.util

class LruCache<K, V>(private val maxCapacity: Int? = null) {
private val internalMap: LinkedHashMap<K, V>
private val lock = Any()

init {
if (maxCapacity == null) {
internalMap = LinkedHashMap()
} else {
internalMap =
object : LinkedHashMap<K, V>(minOf(16, maxCapacity + 1), 0.75f, true) {
override fun removeEldestEntry(eldest: Map.Entry<K, V>): Boolean {
return size > maxCapacity
}
}
}
}

operator fun get(key: K): V? = internalMap[key]

operator fun set(key: K, value: V) = put(key, value)

Check warning on line 22 in src/main/java/it/smartphonecombo/uecapabilityparser/util/LruCache.kt

View check run for this annotation

Codecov / codecov/patch

src/main/java/it/smartphonecombo/uecapabilityparser/util/LruCache.kt#L22

Added line #L22 was not covered by tests

fun put(key: K, value: V, skipIfFull: Boolean = false): Boolean {
if (maxCapacity == 0 || skipIfFull && full()) return false
synchronized(lock) { internalMap[key] = value }
return true
}

private fun full(): Boolean = maxCapacity == internalMap.size
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package it.smartphonecombo.uecapabilityparser.util

import it.smartphonecombo.uecapabilityparser.extension.trimToSize
import it.smartphonecombo.uecapabilityparser.model.Capabilities
import it.smartphonecombo.uecapabilityparser.model.band.IBandDetails
import it.smartphonecombo.uecapabilityparser.model.combo.ICombo
import it.smartphonecombo.uecapabilityparser.model.component.ComponentNr
import it.smartphonecombo.uecapabilityparser.model.component.IComponent

internal fun Capabilities.optimize() {
lteBands.forEach { it.optimize() }
nrBands.forEach { it.optimize() }
lteCombos.forEach { it.optimize() }
nrCombos.forEach { it.optimize() }
nrDcCombos.forEach { it.optimize() }
enDcCombos.forEach { it.optimize() }
}

internal fun IBandDetails.optimize() {
mimoDL = mimoDL.intern()
mimoUL = mimoUL.intern()
modDL = modDL.intern()
modUL = modUL.intern()
}

internal fun ICombo.optimize() {
masterComponents.trimToSize()
masterComponents.forEach { it.optimize() }
secondaryComponents.trimToSize()
secondaryComponents.forEach { it.optimize() }
}

internal fun IComponent.optimize() {
mimoDL = mimoDL.intern()
mimoUL = mimoUL.intern()
modDL = modDL.intern()
modUL = modUL.intern()
if (this is ComponentNr) {
maxBandwidthDl = maxBandwidthDl.intern()
maxBandwidthUl = maxBandwidthUl.intern()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ internal class ServerModeMultiStoreTest {
@Test
fun getMultiOutput() {
Config["store"] = "$resourcesPath/oracleForMultiStore"
Config["cache"] = "-1"
getTest(
"${endpointStore}getMultiOutput?id=${storedMultiId}",
"$resourcesPath/oracleForMultiStore/multiParseOutput.json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ internal class ServerModeStoreTest {
@Test
fun emptyList() {
Config["store"] = tmpStorePath
Config["cache"] = "0"
getTest(endpointStore + "list", "$resourcesPath/oracleForStore/emptyList.json")
}

@Test
fun storeElement() {
Config["store"] = tmpStorePath
Config["cache"] = "-1"
val oracle = "$resourcesPath/oracleForStore/output/$storedId.json"
val input0 = "$resourcesPath/oracleForStore/input/$storedId-0"
val input1 = "$resourcesPath/oracleForStore/input/$storedId-1"
Expand Down Expand Up @@ -121,6 +123,7 @@ internal class ServerModeStoreTest {

@Test
fun getOutput() {
Config["cache"] = "1000"
Config["store"] = "$resourcesPath/oracleForStore"
getTest(
"${endpointStore}getOutput?id=$storedId",
Expand Down
Loading