Skip to content

Commit a858885

Browse files
committed
cache compiled path tokens
1 parent 5febf9b commit a858885

File tree

13 files changed

+232
-21
lines changed

13 files changed

+232
-21
lines changed

src/main/kotlin/com/nfeld/jsonpathlite/JsonPath.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
package com.nfeld.jsonpathlite
22

3+
import com.nfeld.jsonpathlite.cache.CacheProvider
34
import org.json.JSONArray
45
import org.json.JSONException
56
import org.json.JSONObject
67

78
class JsonPath(path: String) {
89

910
private val path: String
10-
private val tokens: List<Token>
11+
internal val tokens: List<Token>
1112

1213
/**
1314
* Trim given path string and compile it on initialization
1415
*/
1516
init {
1617
this.path = path.trim()
17-
tokens = PathCompiler.compile(this.path)
18+
19+
val cache = CacheProvider.getCache()
20+
val cachedJsonPath = cache?.get(this.path)
21+
if (cachedJsonPath != null) {
22+
tokens = cachedJsonPath.tokens
23+
} else {
24+
tokens = PathCompiler.compile(this.path)
25+
cache?.put(this.path, this)
26+
}
1827
}
1928

2029
/**
@@ -116,7 +125,6 @@ class JsonPath(path: String) {
116125
} catch (e: JSONException) {
117126
null
118127
}
119-
120128
}
121129
}
122130
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.nfeld.jsonpathlite.cache
2+
3+
import com.nfeld.jsonpathlite.JsonPath
4+
5+
interface Cache {
6+
/**
7+
* Retrieve an instance of [JsonPath] containing the compiled path.
8+
*
9+
* @param path path string key for cache
10+
* @return cached [JsonPath] instance or null if not cached
11+
*/
12+
fun get(path: String): JsonPath?
13+
14+
/**
15+
* Insert the given path and [JsonPath] as key/value pair into cache.
16+
*
17+
* @param path path string key for cache
18+
* @param jsonPath instance of [JsonPath] containing compiled path
19+
*/
20+
fun put(path: String, jsonPath: JsonPath)
21+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.nfeld.jsonpathlite.cache
2+
3+
object CacheProvider {
4+
5+
private var cache: Cache? = null
6+
private var useDefault = true
7+
8+
/**
9+
* Consumer can set this to preferred max cache size.
10+
*/
11+
@JvmStatic
12+
var maxCacheSize = 100
13+
14+
/**
15+
* Set cache to custom implementation of [Cache].
16+
*
17+
* @param newCache cache implementation to use, or null if no cache desired.
18+
*/
19+
@JvmStatic
20+
fun setCache(newCache: Cache?) {
21+
useDefault = false
22+
cache = newCache
23+
}
24+
25+
internal fun getCache(): Cache? {
26+
if (cache == null && useDefault) {
27+
synchronized(this) {
28+
if (cache == null) {
29+
cache = createDefaultCache()
30+
}
31+
}
32+
}
33+
return cache
34+
}
35+
36+
private fun createDefaultCache(): Cache = LRUCache(maxCacheSize)
37+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.nfeld.jsonpathlite.cache
2+
3+
import com.nfeld.jsonpathlite.JsonPath
4+
import org.jetbrains.annotations.TestOnly
5+
import java.util.LinkedHashMap
6+
7+
internal class LRUCache(private val maxCacheSize: Int): Cache {
8+
private val map = object : LinkedHashMap<String, JsonPath>(INITIAL_CAPACITY, LOAD_FACTOR, true) {
9+
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, JsonPath>?): Boolean = size > maxCacheSize
10+
}
11+
12+
@Synchronized
13+
override fun get(path: String): JsonPath? = map.get(path)
14+
15+
@Synchronized
16+
override fun put(path: String, jsonPath: JsonPath) {
17+
map.put(path, jsonPath)
18+
}
19+
20+
companion object {
21+
private const val INITIAL_CAPACITY = 16
22+
private const val LOAD_FACTOR = 0.75f
23+
}
24+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.nfeld.jsonpathlite
2+
3+
import com.nfeld.jsonpathlite.cache.CacheProvider
4+
import org.junit.jupiter.api.BeforeAll
5+
6+
open class BaseNoCacheTest: BaseTest() {
7+
8+
companion object {
9+
@JvmStatic
10+
@BeforeAll
11+
fun setupClass() {
12+
println("Disabling cache")
13+
CacheProvider.setCache(null)
14+
}
15+
}
16+
17+
}

src/test/kotlin/com/nfeld/jsonpathlite/BaseTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.nfeld.jsonpathlite
22

3+
import com.nfeld.jsonpathlite.cache.CacheProvider
34
import org.json.JSONArray
5+
import org.junit.jupiter.api.BeforeAll
46

57
open class BaseTest {
68

@@ -11,6 +13,21 @@ open class BaseTest {
1113
}
1214

1315
companion object {
16+
// we need to reset this singleton across test suites
17+
fun resetCacheProvider() {
18+
println("Resetting CacheProvider singleton")
19+
20+
// use reflection to reset CacheProvider singleton to its initial state
21+
CacheProvider.javaClass.getDeclaredField("cache").apply {
22+
isAccessible = true
23+
set(null, null)
24+
}
25+
CacheProvider.javaClass.getDeclaredField("useDefault").apply {
26+
isAccessible = true
27+
set(null, true)
28+
}
29+
}
30+
1431
const val SMALL_JSON = "{\"key\": 5}"
1532
const val SMALL_JSON_ARRAY = "[1,2,3,4, $SMALL_JSON]"
1633
const val LARGE_JSON = "[\n" +

src/test/kotlin/com/nfeld/jsonpathlite/BenchmarkTest.kt

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package com.nfeld.jsonpathlite
22

3-
import com.jayway.jsonpath.spi.cache.CacheProvider
43
import com.jayway.jsonpath.spi.cache.NOOPCache
4+
import com.nfeld.jsonpathlite.cache.CacheProvider
55
import com.nfeld.jsonpathlite.extension.read
66
import org.json.JSONArray
77
import org.junit.jupiter.api.BeforeAll
8-
import org.junit.jupiter.api.Disabled
8+
import org.junit.jupiter.api.BeforeEach
99
import org.junit.jupiter.api.Test
1010

1111
class BenchmarkTest : BaseTest() {
@@ -20,11 +20,14 @@ class BenchmarkTest : BaseTest() {
2020
fun setupClass() {
2121
println("Setting up BenchmarkTest")
2222

23-
// disable JsonPath cache for fair benchmarks
24-
CacheProvider.setCache(NOOPCache())
25-
2623
printReadmeFormat = System.getProperty("readmeFormat")?.toBoolean() ?: false
2724
}
25+
26+
@JvmStatic
27+
@BeforeEach
28+
fun resetCache() {
29+
resetCacheProvider()
30+
}
2831
}
2932

3033
private val timestamp: Long
@@ -59,12 +62,20 @@ class BenchmarkTest : BaseTest() {
5962
}
6063

6164
private fun runBenchmarksAndPrintResults(path: String, callsPerRun: Int = DEFAULT_CALLS_PER_RUN, runs: Int = DEFAULT_RUNS) {
65+
// first benchmarks will be using caches
6266
val lite = benchmarkJsonPathLite(path, callsPerRun, runs)
6367
val other = benchmarkJsonPath(path, callsPerRun, runs)
68+
69+
// now disable caches
70+
CacheProvider.setCache(null)
71+
com.jayway.jsonpath.spi.cache.CacheProvider.setCache(NOOPCache())
72+
val liteNoCache = benchmarkJsonPathLite(path, callsPerRun, runs)
73+
val otherNoCache = benchmarkJsonPath(path, callsPerRun, runs)
74+
6475
if (printReadmeFormat) {
65-
println("| $path | ${lite} ms | ${other} ms |")
76+
println("| $path | ${lite} ms | ${other} ms | $liteNoCache ms | $otherNoCache ms |")
6677
} else {
67-
println("$path lite: ${lite}, jsonpath: ${other}")
78+
println("$path lite: ${lite}, jsonpath: ${other} Without caches: lite: ${liteNoCache}, jsonpath: ${otherNoCache}")
6879
}
6980
}
7081

@@ -82,15 +93,23 @@ class BenchmarkTest : BaseTest() {
8293
fun benchmarkPathCompile() {
8394

8495
fun compile(path: String) {
96+
// first with caches
8597
val lite = benchmark { JsonPath(path) }
8698
val other = benchmark { com.jayway.jsonpath.JsonPath.compile(path) }
99+
100+
// now disable caches
101+
CacheProvider.setCache(null)
102+
com.jayway.jsonpath.spi.cache.CacheProvider.setCache(NOOPCache())
103+
val liteNoCache = benchmark { JsonPath(path) }
104+
val otherNoCache = benchmark { com.jayway.jsonpath.JsonPath.compile(path) }
105+
87106
val numTokens = PathCompiler.compile(path).size
88107
val name = "${path.length} chars, $numTokens tokens"
89108

90109
if (printReadmeFormat) {
91-
println("| $name | ${lite} ms | ${other} ms |")
110+
println("| $name | ${lite} ms | ${other} ms | $liteNoCache ms | $otherNoCache ms |")
92111
} else {
93-
println("$name lite: ${lite}, jsonpath: ${other}")
112+
println("$name lite: ${lite}, jsonpath: ${other} Without caches: lite: ${liteNoCache}, jsonpath: ${otherNoCache}")
94113
}
95114
}
96115

@@ -142,11 +161,11 @@ class BenchmarkTest : BaseTest() {
142161

143162
@Test
144163
fun benchmarkMultiArrayAccess() {
145-
runBenchmarksAndPrintResults("$[0]['tags'][0,3, 5]")
164+
runBenchmarksAndPrintResults("$[0]['tags'][0,3,5]")
146165
}
147166

148167
@Test
149168
fun benchmarkMultiObjectAccess() {
150-
runBenchmarksAndPrintResults("$[0]['latitude','longitude', 'isActive']")
169+
runBenchmarksAndPrintResults("$[0]['latitude','longitude','isActive']")
151170
}
152171
}

src/test/kotlin/com/nfeld/jsonpathlite/JsonPathTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import org.junit.jupiter.api.Assertions.*
88
import org.junit.jupiter.api.Test
99
import org.junit.jupiter.api.assertThrows
1010

11-
class JsonPathTest : BaseTest() {
11+
class JsonPathTest : BaseNoCacheTest() {
1212

1313
// JsonPath::parse related tests
1414
@Test

src/test/kotlin/com/nfeld/jsonpathlite/PathCompilerTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
44
import org.junit.jupiter.api.Test
55
import org.junit.jupiter.api.assertThrows
66

7-
class PathCompilerTest : BaseTest() {
7+
class PathCompilerTest : BaseNoCacheTest() {
88

99
@Test
1010
fun compile() {

src/test/kotlin/com/nfeld/jsonpathlite/TokenTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
66
import org.junit.jupiter.api.Assertions.assertNull
77
import org.junit.jupiter.api.Test
88

9-
class TokenTest : BaseTest() {
9+
class TokenTest : BaseNoCacheTest() {
1010

1111
private fun printTesting(subpath: String) {
1212
println("Testing like $subpath")

0 commit comments

Comments
 (0)