Skip to content

Commit 56e241a

Browse files
authored
Better key uri router perf (#69)
* WIP * numpang * avoid overfitted in key uri router * complete the doc * clean up
1 parent 60265b4 commit 56e241a

7 files changed

Lines changed: 80 additions & 40 deletions

File tree

android/src/main/java/nolambda/linkrouter/android/UriRouterFactory.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package nolambda.linkrouter.android
22

3-
import nolambda.linkrouter.DeepLinkUri
3+
import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUri
44
import nolambda.linkrouter.KeyUriRouter
55
import nolambda.linkrouter.SimpleUriRouter
66
import nolambda.linkrouter.UriRouter
@@ -9,7 +9,10 @@ import java.util.concurrent.ConcurrentHashMap
99

1010
class KeyUriRouterFactory(
1111
private val logger: UriRouterLogger? = RouterPlugin.logger,
12-
private val keyExtractor: (DeepLinkUri) -> String = { uri -> "${uri.scheme}${uri.host}" }
12+
private val keyExtractor: (String) -> String = { it ->
13+
val uri = it.toDeepLinkUri()
14+
"${uri.scheme}${uri.host}"
15+
}
1316
) : UriRouterFactory {
1417
override fun create(): UriRouter<UriResult> {
1518
return KeyUriRouter(logger, keyExtractor)

core/src/main/java/nolambda/linkrouter/KeyUriRouter.kt

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,58 @@
11
package nolambda.linkrouter
22

3-
import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUri
43
import nolambda.linkrouter.matcher.UriMatcher
54

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

17-
private val keyToUriMap = mutableMapOf<String, MutableSet<Pair<DeepLinkUri, UriMatcher>>>()
18-
private val keyToEntryMap = mutableMapOf<String, MutableSet<Pair<DeepLinkEntry, UriMatcher>>>()
23+
/**
24+
* Key: From key extractor. Usually scheme + host + path
25+
* Value: Set of Pair of [DeepLinkEntry] and the matcher [UriMatcher]
26+
*
27+
* The result from here will be moved to [keyToEntryMap]
28+
* and the entry will be deleted after that
29+
*/
30+
private val keyToUriMap = mutableMapOf<String, MutableSet<Pair<String, UriMatcher>>>()
31+
32+
/**
33+
* Key: From key extractor. Usually scheme + host + path
34+
* Value: Set of Pair of [DeepLinkEntry] and the matcher [UriMatcher]
35+
*
36+
* If there's a value found in here, we don't need to search in [keyToUriMap]
37+
*/
38+
private val keyToEntryMap =
39+
mutableMapOf<String, MutableSet<Triple<DeepLinkEntry, String, UriMatcher>>>()
1940

20-
private val handlerMap = mutableMapOf<DeepLinkUri, UriRouterHandler<URI>>()
41+
/**
42+
* Key: Registered route
43+
* Value: Registered lambda
44+
*
45+
* If we found registered route (key) from [keyToEntryMap] or [keyToUriMap]
46+
* we will find the registered lambda in here
47+
*/
48+
private val handlerMap = mutableMapOf<String, UriRouterHandler<URI>>()
2149

2250
override fun clear() {
2351
keyToUriMap.clear()
2452
}
2553

2654
override fun resolveEntry(route: String): Pair<DeepLinkEntry, EntryValue<URI>>? {
27-
val deepLinkUri = route.toDeepLinkUri()
28-
val key = keyExtractor(deepLinkUri)
55+
val key = keyExtractor(route)
2956

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

4875
var entry: DeepLinkEntry? = null
4976
var matcher: UriMatcher? = null
50-
var pairOfUriAndMatcher: Pair<DeepLinkUri, UriMatcher>? = null
77+
var pairOfUriAndMatcher: Pair<String, UriMatcher>? = null
5178

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

6390
// Add parsed entry to entry map
6491
val sets = keyToEntryMap.getOrPut(key) { mutableSetOf() }
65-
sets.add(currentEntry to currentMatcher)
92+
sets.add(Triple(currentEntry, pair.first, currentMatcher))
6693

6794
currentMatcher.match(currentEntry, route)
6895
} ?: return null
@@ -85,25 +112,26 @@ class KeyUriRouter<URI>(
85112
route: String
86113
): Pair<DeepLinkEntry, EntryValue<URI>>? {
87114
val set = keyToEntryMap[key] ?: return null
88-
val actualKey = set.firstOrNull { (entry, matcher) ->
115+
val actualKey = set.firstOrNull { (entry, _, matcher) ->
89116
matcher.match(entry, route)
90117
} ?: return null
91118

92-
return createResult(actualKey.first.uri, actualKey.first, actualKey.second)
119+
return createResult(actualKey.second, actualKey.first, actualKey.third)
93120
}
94121

95122
private fun createResult(
96-
keyUri: DeepLinkUri,
123+
registeredRoute: String,
97124
entry: DeepLinkEntry?,
98125
matcher: UriMatcher?
99126
): Pair<DeepLinkEntry, EntryValue<URI>> {
100-
val handler = handlerMap[keyUri] ?: error("Handler not available for $keyUri")
127+
val handler = handlerMap[registeredRoute]
128+
?: error("Handler not available for $registeredRoute")
101129

102130
return Pair(
103-
first = entry ?: error("Entry not available fro $keyUri"),
131+
first = entry ?: error("Entry not available fro $registeredRoute"),
104132
second = EntryValue(
105133
handler,
106-
matcher ?: error("Matcher not available fro $keyUri")
134+
matcher ?: error("Matcher not available fro $registeredRoute")
107135
)
108136
)
109137
}
@@ -112,22 +140,19 @@ class KeyUriRouter<URI>(
112140
* This is not thread-safe
113141
*/
114142
override fun addEntry(
115-
vararg uri: String,
143+
vararg uris: String,
116144
matcher: UriMatcher,
117145
handler: UriRouterHandler<URI>
118146
) {
119-
uri.forEach {
120-
val deepLinkUri = it.toDeepLinkUri()
121-
val key = keyExtractor(deepLinkUri)
122-
123-
inputToEntryContainer(key, deepLinkUri, matcher)
124-
125-
handlerMap[deepLinkUri] = handler
147+
uris.forEach { route ->
148+
val key = keyExtractor(route)
149+
inputToEntryContainer(key, route, matcher)
150+
handlerMap[route] = handler
126151
}
127152
}
128153

129-
private fun inputToEntryContainer(key: String, uri: DeepLinkUri, matcher: UriMatcher) {
154+
private fun inputToEntryContainer(key: String, registeredRoute: String, matcher: UriMatcher) {
130155
val entriesHolder = keyToUriMap.getOrPut(key) { mutableSetOf() }
131-
entriesHolder.add(uri to matcher)
156+
entriesHolder.add(registeredRoute to matcher)
132157
}
133158
}

core/src/main/java/nolambda/linkrouter/SimpleUriRouter.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ class SimpleUriRouter<RES>(
1818
}?.toPair()
1919
}
2020

21-
override fun addEntry(vararg uri: String, matcher: UriMatcher, handler: UriRouterHandler<RES>) {
22-
val deepLinkEntries = uri.map { DeepLinkEntry.parse(it) }
21+
override fun addEntry(vararg uris: String, matcher: UriMatcher, handler: UriRouterHandler<RES>) {
22+
val deepLinkEntries = uris.map { DeepLinkEntry.parse(it) }
2323
deepLinkEntries.forEach { entry ->
2424
entries[entry] = EntryValue(handler, matcher)
2525
}

core/src/main/java/nolambda/linkrouter/UriRouter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ abstract class UriRouter<RES>(
3636
}
3737

3838
abstract fun addEntry(
39-
vararg uri: String,
39+
vararg uris: String,
4040
matcher: UriMatcher = DeepLinkEntryMatcher,
4141
handler: UriRouterHandler<RES>
4242
)

core/src/test/java/nolambda/linkrouter/PerformanceTest.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
package nolambda.linkrouter
22

33
import io.kotest.core.spec.style.StringSpec
4-
import java.util.Random
4+
import nolambda.linkrouter.DeepLinkUri.Companion.toDeepLinkUri
5+
import java.util.*
56
import kotlin.system.measureTimeMillis
67

78
class PerformanceTest : StringSpec({
89

9-
val size = 500L
10+
val size = 20_000L
1011
val random = Random(size)
1112

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

1415
val simpleRouter = SimpleUriRouter<Unit>(logger)
1516
val keyRouter = KeyUriRouter<Unit>(logger) {
16-
"${it.scheme}${it.host}${it.pathSegments.size}"
17+
val deepLinkUri = it.toDeepLinkUri()
18+
"${deepLinkUri.scheme}${deepLinkUri.host}${deepLinkUri.pathSegments.size}"
1719
}
1820

1921
val generateEntry = {

core/src/test/java/nolambda/linkrouter/UriRouterSpec.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ class UriRouterSpec : StringSpec({
1010

1111
val routers = listOf(
1212
SimpleUriRouter<String>(),
13-
KeyUriRouter { "${it.scheme}${it.host}" }
13+
KeyUriRouter {
14+
val uri = it.toDeepLinkUri()
15+
"${uri.scheme}${uri.host}"
16+
}
1417
)
1518

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

2933
println("Using $router")

samples/app/src/main/java/nolambda/linkrouter/examples/PerformanceTestActivity.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import nolambda.linkrouter.android.SimpleUriRouterFactory
1212
import nolambda.linkrouter.android.registerstrategy.EagerRegisterStrategy
1313
import nolambda.linkrouter.android.registerstrategy.LazyRegisterStrategy
1414
import nolambda.linkrouter.examples.utils.isDebuggable
15+
import nolambda.linkrouter.matcher.DeepLinkEntryMatcher
1516
import kotlin.random.Random
1617
import kotlin.system.measureTimeMillis
1718

@@ -58,7 +59,7 @@ class PerformanceTestActivity : AppCompatActivity() {
5859
private fun createRoutes(havePath: Boolean): List<Route> {
5960
if (havePath) {
6061
return (0 until ROUTES_SIZE).map {
61-
object : Route("https://test.com/${Random.nextInt(5)}/$it") {}
62+
object : Route("https://test.com/${Random.nextInt(5)}/$it/{a}") {}
6263
}
6364
}
6465
return (0 until ROUTES_SIZE).map {
@@ -94,8 +95,10 @@ class PerformanceTestActivity : AppCompatActivity() {
9495
false -> EagerRegisterStrategy()
9596
},
9697
uriRouterFactory = when (isKeyUri) {
97-
true -> KeyUriRouterFactory(logger) { uri ->
98-
"${uri.scheme}${uri.host}${uri.pathSegments.joinToString()}"
98+
true -> KeyUriRouterFactory(logger) {
99+
val uri = java.net.URL(it)
100+
val paths = uri.path.split("/")
101+
"${paths[1]}${paths[2]}"
99102
}
100103
false -> SimpleUriRouterFactory(logger)
101104
}
@@ -106,13 +109,16 @@ class PerformanceTestActivity : AppCompatActivity() {
106109
val t1 = measureWithPrint(
107110
log = { time -> "$tag registers took $time ms for $size entries" },
108111
block = {
109-
routes.forEach {
110-
testRouter.addEntry(it)
112+
routes.forEach { route ->
113+
testRouter.addEntry(route)
111114
}
112115
}
113116
)
114117

115-
val route = routes.random().routePaths.firstOrNull() ?: return
118+
// Get test route and assign the variable
119+
val route = routes.random().routePaths.first()
120+
.replace("{a}", Random.nextInt().toString())
121+
116122
val t2 = measureWithPrint(
117123
log = { time -> "$tag goTo took $time ms to resolve from $size entries" },
118124
block = { testRouter.goTo(route) }

0 commit comments

Comments
 (0)