From 56e241a6b5291a49066188f96039063296ec7812 Mon Sep 17 00:00:00 2001 From: Esa Firman Date: Tue, 7 Dec 2021 13:08:24 +0700 Subject: [PATCH] Better key uri router perf (#69) * WIP * numpang * avoid overfitted in key uri router * complete the doc * clean up --- .../linkrouter/android/UriRouterFactory.kt | 7 +- .../java/nolambda/linkrouter/KeyUriRouter.kt | 75 ++++++++++++------- .../nolambda/linkrouter/SimpleUriRouter.kt | 4 +- .../java/nolambda/linkrouter/UriRouter.kt | 2 +- .../nolambda/linkrouter/PerformanceTest.kt | 8 +- .../java/nolambda/linkrouter/UriRouterSpec.kt | 6 +- .../examples/PerformanceTestActivity.kt | 18 +++-- 7 files changed, 80 insertions(+), 40 deletions(-) diff --git a/android/src/main/java/nolambda/linkrouter/android/UriRouterFactory.kt b/android/src/main/java/nolambda/linkrouter/android/UriRouterFactory.kt index dd29de2..4ef7f44 100644 --- a/android/src/main/java/nolambda/linkrouter/android/UriRouterFactory.kt +++ b/android/src/main/java/nolambda/linkrouter/android/UriRouterFactory.kt @@ -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 @@ -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 { return KeyUriRouter(logger, keyExtractor) diff --git a/core/src/main/java/nolambda/linkrouter/KeyUriRouter.kt b/core/src/main/java/nolambda/linkrouter/KeyUriRouter.kt index d628606..dda6af8 100644 --- a/core/src/main/java/nolambda/linkrouter/KeyUriRouter.kt +++ b/core/src/main/java/nolambda/linkrouter/KeyUriRouter.kt @@ -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( private val logger: UriRouterLogger? = null, - private val keyExtractor: (DeepLinkUri) -> String + private val keyExtractor: (String) -> String ) : UriRouter(logger) { - private val keyToUriMap = mutableMapOf>>() - private val keyToEntryMap = mutableMapOf>>() + /** + * 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>>() + + /** + * 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>>() - private val handlerMap = mutableMapOf>() + /** + * 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>() override fun clear() { keyToUriMap.clear() } override fun resolveEntry(route: String): Pair>? { - val deepLinkUri = route.toDeepLinkUri() - val key = keyExtractor(deepLinkUri) + val key = keyExtractor(route) logger?.run { invoke("Key: $key") @@ -47,7 +74,7 @@ class KeyUriRouter( var entry: DeepLinkEntry? = null var matcher: UriMatcher? = null - var pairOfUriAndMatcher: Pair? = null + var pairOfUriAndMatcher: Pair? = null val uriList = keyToUriMap[key] ?: return null val actualKey = uriList.firstOrNull { pair -> @@ -62,7 +89,7 @@ class KeyUriRouter( // 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 @@ -85,25 +112,26 @@ class KeyUriRouter( route: String ): Pair>? { 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> { - 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") ) ) } @@ -112,22 +140,19 @@ class KeyUriRouter( * This is not thread-safe */ override fun addEntry( - vararg uri: String, + vararg uris: String, matcher: UriMatcher, handler: UriRouterHandler ) { - 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) } } \ No newline at end of file diff --git a/core/src/main/java/nolambda/linkrouter/SimpleUriRouter.kt b/core/src/main/java/nolambda/linkrouter/SimpleUriRouter.kt index 9a03d28..acffa29 100644 --- a/core/src/main/java/nolambda/linkrouter/SimpleUriRouter.kt +++ b/core/src/main/java/nolambda/linkrouter/SimpleUriRouter.kt @@ -18,8 +18,8 @@ class SimpleUriRouter( }?.toPair() } - override fun addEntry(vararg uri: String, matcher: UriMatcher, handler: UriRouterHandler) { - val deepLinkEntries = uri.map { DeepLinkEntry.parse(it) } + override fun addEntry(vararg uris: String, matcher: UriMatcher, handler: UriRouterHandler) { + val deepLinkEntries = uris.map { DeepLinkEntry.parse(it) } deepLinkEntries.forEach { entry -> entries[entry] = EntryValue(handler, matcher) } diff --git a/core/src/main/java/nolambda/linkrouter/UriRouter.kt b/core/src/main/java/nolambda/linkrouter/UriRouter.kt index 773dcd8..c933fec 100644 --- a/core/src/main/java/nolambda/linkrouter/UriRouter.kt +++ b/core/src/main/java/nolambda/linkrouter/UriRouter.kt @@ -36,7 +36,7 @@ abstract class UriRouter( } abstract fun addEntry( - vararg uri: String, + vararg uris: String, matcher: UriMatcher = DeepLinkEntryMatcher, handler: UriRouterHandler ) diff --git a/core/src/test/java/nolambda/linkrouter/PerformanceTest.kt b/core/src/test/java/nolambda/linkrouter/PerformanceTest.kt index 7be0cb8..f7d9bce 100644 --- a/core/src/test/java/nolambda/linkrouter/PerformanceTest.kt +++ b/core/src/test/java/nolambda/linkrouter/PerformanceTest.kt @@ -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(logger) val keyRouter = KeyUriRouter(logger) { - "${it.scheme}${it.host}${it.pathSegments.size}" + val deepLinkUri = it.toDeepLinkUri() + "${deepLinkUri.scheme}${deepLinkUri.host}${deepLinkUri.pathSegments.size}" } val generateEntry = { diff --git a/core/src/test/java/nolambda/linkrouter/UriRouterSpec.kt b/core/src/test/java/nolambda/linkrouter/UriRouterSpec.kt index ea3110e..3c1bd19 100644 --- a/core/src/test/java/nolambda/linkrouter/UriRouterSpec.kt +++ b/core/src/test/java/nolambda/linkrouter/UriRouterSpec.kt @@ -10,7 +10,10 @@ class UriRouterSpec : StringSpec({ val routers = listOf( SimpleUriRouter(), - KeyUriRouter { "${it.scheme}${it.host}" } + KeyUriRouter { + val uri = it.toDeepLinkUri() + "${uri.scheme}${uri.host}" + } ) "it should match and resolve to respected path" { @@ -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") diff --git a/samples/app/src/main/java/nolambda/linkrouter/examples/PerformanceTestActivity.kt b/samples/app/src/main/java/nolambda/linkrouter/examples/PerformanceTestActivity.kt index 308fe04..0f263a8 100644 --- a/samples/app/src/main/java/nolambda/linkrouter/examples/PerformanceTestActivity.kt +++ b/samples/app/src/main/java/nolambda/linkrouter/examples/PerformanceTestActivity.kt @@ -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 @@ -58,7 +59,7 @@ class PerformanceTestActivity : AppCompatActivity() { private fun createRoutes(havePath: Boolean): List { 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 { @@ -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) } @@ -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) }