Skip to content

Commit

Permalink
Better key uri router perf (#69)
Browse files Browse the repository at this point in the history
* WIP

* numpang

* avoid overfitted in key uri router

* complete the doc

* clean up
  • Loading branch information
esafirm authored Dec 7, 2021
1 parent 60265b4 commit 56e241a
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package nolambda.linkrouter.android

import nolambda.linkrouter.DeepLinkUri
import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUri
import nolambda.linkrouter.KeyUriRouter
import nolambda.linkrouter.SimpleUriRouter
import nolambda.linkrouter.UriRouter
Expand All @@ -9,7 +9,10 @@ import java.util.concurrent.ConcurrentHashMap

class KeyUriRouterFactory(
private val logger: UriRouterLogger? = RouterPlugin.logger,
private val keyExtractor: (DeepLinkUri) -> String = { uri -> "${uri.scheme}${uri.host}" }
private val keyExtractor: (String) -> String = { it ->
val uri = it.toDeepLinkUri()
"${uri.scheme}${uri.host}"
}
) : UriRouterFactory {
override fun create(): UriRouter<UriResult> {
return KeyUriRouter(logger, keyExtractor)
Expand Down
75 changes: 50 additions & 25 deletions core/src/main/java/nolambda/linkrouter/KeyUriRouter.kt
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
package nolambda.linkrouter

import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUri
import nolambda.linkrouter.matcher.UriMatcher

/**
* router that use "key" to group [DeepLinkUri] or [DeepLinkEntry] for faster
* register and lookup
*
* Flow:
* 1. Register the route to [handlerMap] and [keyToUriMap]
* 2. When resolving, it first search in [keyToEntryMap]
* a. If no match, then search to [keyToUriMap], this will producing [DeepLinkEntry] and add it to [keyToEntryMap]
* b. If match it will resulting the registered route
* 3. Get the handler from [handlerMap] by using the registered route
*
* Please note, this router addition process is not thread-safe.
*/
class KeyUriRouter<URI>(
private val logger: UriRouterLogger? = null,
private val keyExtractor: (DeepLinkUri) -> String
private val keyExtractor: (String) -> String
) : UriRouter<URI>(logger) {

private val keyToUriMap = mutableMapOf<String, MutableSet<Pair<DeepLinkUri, UriMatcher>>>()
private val keyToEntryMap = mutableMapOf<String, MutableSet<Pair<DeepLinkEntry, UriMatcher>>>()
/**
* Key: From key extractor. Usually scheme + host + path
* Value: Set of Pair of [DeepLinkEntry] and the matcher [UriMatcher]
*
* The result from here will be moved to [keyToEntryMap]
* and the entry will be deleted after that
*/
private val keyToUriMap = mutableMapOf<String, MutableSet<Pair<String, UriMatcher>>>()

/**
* Key: From key extractor. Usually scheme + host + path
* Value: Set of Pair of [DeepLinkEntry] and the matcher [UriMatcher]
*
* If there's a value found in here, we don't need to search in [keyToUriMap]
*/
private val keyToEntryMap =
mutableMapOf<String, MutableSet<Triple<DeepLinkEntry, String, UriMatcher>>>()

private val handlerMap = mutableMapOf<DeepLinkUri, UriRouterHandler<URI>>()
/**
* Key: Registered route
* Value: Registered lambda
*
* If we found registered route (key) from [keyToEntryMap] or [keyToUriMap]
* we will find the registered lambda in here
*/
private val handlerMap = mutableMapOf<String, UriRouterHandler<URI>>()

override fun clear() {
keyToUriMap.clear()
}

override fun resolveEntry(route: String): Pair<DeepLinkEntry, EntryValue<URI>>? {
val deepLinkUri = route.toDeepLinkUri()
val key = keyExtractor(deepLinkUri)
val key = keyExtractor(route)

logger?.run {
invoke("Key: $key")
Expand All @@ -47,7 +74,7 @@ class KeyUriRouter<URI>(

var entry: DeepLinkEntry? = null
var matcher: UriMatcher? = null
var pairOfUriAndMatcher: Pair<DeepLinkUri, UriMatcher>? = null
var pairOfUriAndMatcher: Pair<String, UriMatcher>? = null

val uriList = keyToUriMap[key] ?: return null
val actualKey = uriList.firstOrNull { pair ->
Expand All @@ -62,7 +89,7 @@ class KeyUriRouter<URI>(

// Add parsed entry to entry map
val sets = keyToEntryMap.getOrPut(key) { mutableSetOf() }
sets.add(currentEntry to currentMatcher)
sets.add(Triple(currentEntry, pair.first, currentMatcher))

currentMatcher.match(currentEntry, route)
} ?: return null
Expand All @@ -85,25 +112,26 @@ class KeyUriRouter<URI>(
route: String
): Pair<DeepLinkEntry, EntryValue<URI>>? {
val set = keyToEntryMap[key] ?: return null
val actualKey = set.firstOrNull { (entry, matcher) ->
val actualKey = set.firstOrNull { (entry, _, matcher) ->
matcher.match(entry, route)
} ?: return null

return createResult(actualKey.first.uri, actualKey.first, actualKey.second)
return createResult(actualKey.second, actualKey.first, actualKey.third)
}

private fun createResult(
keyUri: DeepLinkUri,
registeredRoute: String,
entry: DeepLinkEntry?,
matcher: UriMatcher?
): Pair<DeepLinkEntry, EntryValue<URI>> {
val handler = handlerMap[keyUri] ?: error("Handler not available for $keyUri")
val handler = handlerMap[registeredRoute]
?: error("Handler not available for $registeredRoute")

return Pair(
first = entry ?: error("Entry not available fro $keyUri"),
first = entry ?: error("Entry not available fro $registeredRoute"),
second = EntryValue(
handler,
matcher ?: error("Matcher not available fro $keyUri")
matcher ?: error("Matcher not available fro $registeredRoute")
)
)
}
Expand All @@ -112,22 +140,19 @@ class KeyUriRouter<URI>(
* This is not thread-safe
*/
override fun addEntry(
vararg uri: String,
vararg uris: String,
matcher: UriMatcher,
handler: UriRouterHandler<URI>
) {
uri.forEach {
val deepLinkUri = it.toDeepLinkUri()
val key = keyExtractor(deepLinkUri)

inputToEntryContainer(key, deepLinkUri, matcher)

handlerMap[deepLinkUri] = handler
uris.forEach { route ->
val key = keyExtractor(route)
inputToEntryContainer(key, route, matcher)
handlerMap[route] = handler
}
}

private fun inputToEntryContainer(key: String, uri: DeepLinkUri, matcher: UriMatcher) {
private fun inputToEntryContainer(key: String, registeredRoute: String, matcher: UriMatcher) {
val entriesHolder = keyToUriMap.getOrPut(key) { mutableSetOf() }
entriesHolder.add(uri to matcher)
entriesHolder.add(registeredRoute to matcher)
}
}
4 changes: 2 additions & 2 deletions core/src/main/java/nolambda/linkrouter/SimpleUriRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class SimpleUriRouter<RES>(
}?.toPair()
}

override fun addEntry(vararg uri: String, matcher: UriMatcher, handler: UriRouterHandler<RES>) {
val deepLinkEntries = uri.map { DeepLinkEntry.parse(it) }
override fun addEntry(vararg uris: String, matcher: UriMatcher, handler: UriRouterHandler<RES>) {
val deepLinkEntries = uris.map { DeepLinkEntry.parse(it) }
deepLinkEntries.forEach { entry ->
entries[entry] = EntryValue(handler, matcher)
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/nolambda/linkrouter/UriRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ abstract class UriRouter<RES>(
}

abstract fun addEntry(
vararg uri: String,
vararg uris: String,
matcher: UriMatcher = DeepLinkEntryMatcher,
handler: UriRouterHandler<RES>
)
Expand Down
8 changes: 5 additions & 3 deletions core/src/test/java/nolambda/linkrouter/PerformanceTest.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package nolambda.linkrouter

import io.kotest.core.spec.style.StringSpec
import java.util.Random
import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUri
import java.util.*
import kotlin.system.measureTimeMillis

class PerformanceTest : StringSpec({

val size = 500L
val size = 20_000L
val random = Random(size)

val logger = { log: String -> println(log) }

val simpleRouter = SimpleUriRouter<Unit>(logger)
val keyRouter = KeyUriRouter<Unit>(logger) {
"${it.scheme}${it.host}${it.pathSegments.size}"
val deepLinkUri = it.toDeepLinkUri()
"${deepLinkUri.scheme}${deepLinkUri.host}${deepLinkUri.pathSegments.size}"
}

val generateEntry = {
Expand Down
6 changes: 5 additions & 1 deletion core/src/test/java/nolambda/linkrouter/UriRouterSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ class UriRouterSpec : StringSpec({

val routers = listOf(
SimpleUriRouter<String>(),
KeyUriRouter { "${it.scheme}${it.host}" }
KeyUriRouter {
val uri = it.toDeepLinkUri()
"${uri.scheme}${uri.host}"
}
)

"it should match and resolve to respected path" {
Expand All @@ -24,6 +27,7 @@ class UriRouterSpec : StringSpec({
"http://test.com/promo-list/item3" to "6",
"http://test.com/promo-list/item4" to "7",
"http://test.com/promo-list/{a}" to "8",
"http://test.com/promo-list/item5" to "8", // Should hit the above URI
)

println("Using $router")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import nolambda.linkrouter.android.SimpleUriRouterFactory
import nolambda.linkrouter.android.registerstrategy.EagerRegisterStrategy
import nolambda.linkrouter.android.registerstrategy.LazyRegisterStrategy
import nolambda.linkrouter.examples.utils.isDebuggable
import nolambda.linkrouter.matcher.DeepLinkEntryMatcher
import kotlin.random.Random
import kotlin.system.measureTimeMillis

Expand Down Expand Up @@ -58,7 +59,7 @@ class PerformanceTestActivity : AppCompatActivity() {
private fun createRoutes(havePath: Boolean): List<Route> {
if (havePath) {
return (0 until ROUTES_SIZE).map {
object : Route("https://test.com/${Random.nextInt(5)}/$it") {}
object : Route("https://test.com/${Random.nextInt(5)}/$it/{a}") {}
}
}
return (0 until ROUTES_SIZE).map {
Expand Down Expand Up @@ -94,8 +95,10 @@ class PerformanceTestActivity : AppCompatActivity() {
false -> EagerRegisterStrategy()
},
uriRouterFactory = when (isKeyUri) {
true -> KeyUriRouterFactory(logger) { uri ->
"${uri.scheme}${uri.host}${uri.pathSegments.joinToString()}"
true -> KeyUriRouterFactory(logger) {
val uri = java.net.URL(it)
val paths = uri.path.split("/")
"${paths[1]}${paths[2]}"
}
false -> SimpleUriRouterFactory(logger)
}
Expand All @@ -106,13 +109,16 @@ class PerformanceTestActivity : AppCompatActivity() {
val t1 = measureWithPrint(
log = { time -> "$tag registers took $time ms for $size entries" },
block = {
routes.forEach {
testRouter.addEntry(it)
routes.forEach { route ->
testRouter.addEntry(route)
}
}
)

val route = routes.random().routePaths.firstOrNull() ?: return
// Get test route and assign the variable
val route = routes.random().routePaths.first()
.replace("{a}", Random.nextInt().toString())

val t2 = measureWithPrint(
log = { time -> "$tag goTo took $time ms to resolve from $size entries" },
block = { testRouter.goTo(route) }
Expand Down

0 comments on commit 56e241a

Please sign in to comment.