Skip to content

Commit ac3ba86

Browse files
authored
Optimize JSON object builder (#216)
* Add JSONValueBuilder * Simplify JSONContext interface * Rename to NullJSONContext * Extract json benchmark to another project (only for Scala 2.12)
1 parent dc96c00 commit ac3ba86

File tree

13 files changed

+364
-377
lines changed

13 files changed

+364
-377
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package wvlet.airframe.json
15+
16+
import wvlet.airframe.AirframeSpec
17+
import wvlet.airframe.json.JSON.{JSONArray, JSONString}
18+
import wvlet.log.io.{IOUtil, Timer}
19+
20+
import scala.util.Random
21+
22+
/**
23+
*
24+
*/
25+
class JSONBenchmark extends AirframeSpec with Timer {
26+
27+
def bench(benchName: String, json: String, N: Int = 5, B: Int = 2): Unit = {
28+
val jsonSource = JSONSource.fromString(json)
29+
time(benchName, repeat = N, blockRepeat = B) {
30+
block("airframe ") {
31+
JSON.parse(jsonSource)
32+
}
33+
block("airframe scan ") {
34+
JSONScanner.scan(jsonSource)
35+
}
36+
block("circe ") {
37+
io.circe.parser.parse(json)
38+
}
39+
block("jawn ") {
40+
new io.circe.jawn.JawnParser().parse(json)
41+
}
42+
block("json4s-jackson") {
43+
org.json4s.jackson.JsonMethods.parse(json)
44+
}
45+
block("json4s-native ") {
46+
org.json4s.native.JsonMethods.parse(json)
47+
}
48+
block("uJson ") {
49+
ujson.read(json)
50+
}
51+
}
52+
}
53+
54+
lazy val twitterJson = IOUtil.readAsString("airframe-json/src/test/resources/twitter.json")
55+
56+
"JSONScannerBenchmarhk" should {
57+
"parse twitter.json" taggedAs ("comparison") in {
58+
bench("twitter.json", twitterJson)
59+
}
60+
61+
"parse boolen arrays" taggedAs ("boolean-array") in {
62+
val jsonArray = s"[${(0 until 10000).map(_ => Random.nextBoolean()).mkString(",")}]"
63+
bench("boolean array", jsonArray)
64+
}
65+
66+
"parse string arrays" taggedAs ("string-array") in {
67+
// Extract JSON strings from twitter.json
68+
val j = JSON.parse(twitterJson)
69+
val b = IndexedSeq.newBuilder[JSONString]
70+
JSONTraverser.traverse(j, new JSONVisitor {
71+
override def visitKeyValue(k: String, v: JSON.JSONValue): Unit = {
72+
b += JSONString(k)
73+
}
74+
override def visitString(v: JSON.JSONString): Unit = {
75+
b += v
76+
}
77+
})
78+
val jsonArray = JSONArray(b.result()).toJSON
79+
bench("string array", jsonArray)
80+
}
81+
}
82+
83+
}

airframe-json/.jvm/src/test/scala/wvlet/airframe/json/JSONScannerBenchmark.scala

Lines changed: 0 additions & 113 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package wvlet.airframe.json
15+
16+
import wvlet.airframe.AirframeSpec
17+
import wvlet.log.io.IOUtil
18+
19+
/**
20+
*
21+
*/
22+
class TwitterJSONTest extends AirframeSpec {
23+
lazy val twitterJson = IOUtil.readAsString("airframe-json/src/test/resources/twitter.json")
24+
25+
"JSONParser" should {
26+
"parser twiter.json as JSONValue" in {
27+
val j = JSON.parse(twitterJson)
28+
}
29+
}
30+
31+
}

airframe-json/src/main/scala/wvlet/airframe/json/JSON.scala

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
*/
1414
package wvlet.airframe.json
1515

16+
import wvlet.log.LogSupport
17+
1618
/**
1719
*
1820
*/
19-
object JSON {
21+
object JSON extends LogSupport {
2022

2123
def parse(s: String): JSONValue = {
2224
parse(JSONSource.fromString(s))
@@ -28,33 +30,37 @@ object JSON {
2830
parse(JSONSource.fromBytes(s, offset, length))
2931
}
3032
def parse(s: JSONSource): JSONValue = {
31-
JSONParser.parse(s)
33+
val b = new JSONValueBuilder().singleContext(s, 0)
34+
JSONScanner.scan(s, b)
35+
val j = b.result
36+
j
3237
}
3338

3439
sealed trait JSONValue {
3540
override def toString = toJSON
3641
def toJSON: String
3742
}
3843

39-
case object JSONNull extends JSONValue {
44+
final case object JSONNull extends JSONValue {
4045
override def toJSON: String = "null"
4146
}
4247

43-
case class JSONBoolean(val v: Boolean) extends JSONValue {
48+
final case class JSONBoolean(val v: Boolean) extends JSONValue {
4449
override def toJSON: String = if (v) "true" else "false"
4550
}
4651

4752
val JSONTrue = JSONBoolean(true)
4853
val JSONFalse = JSONBoolean(false)
4954

5055
trait JSONNumber extends JSONValue
51-
case class JSONDouble(v: Double) extends JSONNumber {
56+
final case class JSONDouble(v: Double) extends JSONNumber {
5257
override def toJSON: String = v.toString
5358
}
54-
case class JSONLong(v: Long) extends JSONNumber {
59+
final case class JSONLong(v: Long) extends JSONNumber {
5560
override def toJSON: String = v.toString
5661
}
57-
case class JSONString(v: String) extends JSONValue {
62+
final case class JSONString(v: String) extends JSONValue {
63+
override def toString = v
5864
override def toJSON: String = {
5965
val s = new StringBuilder(v.length + 2)
6066
s.append("\"")
@@ -64,7 +70,7 @@ object JSON {
6470
}
6571
}
6672

67-
case class JSONObject(v: Seq[(String, JSONValue)]) extends JSONValue {
73+
final case class JSONObject(v: Seq[(String, JSONValue)]) extends JSONValue {
6874
override def toJSON: String = {
6975
val s = new StringBuilder
7076
s.append("{")
@@ -83,7 +89,7 @@ object JSON {
8389
s.result()
8490
}
8591
}
86-
case class JSONArray(v: Seq[JSONValue]) extends JSONValue {
92+
final case class JSONArray(v: IndexedSeq[JSONValue]) extends JSONValue {
8793
override def toJSON: String = {
8894
val s = new StringBuilder
8995
s.append("[")

airframe-json/src/main/scala/wvlet/airframe/json/JSONEventHandler.scala

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,29 @@
1313
*/
1414
package wvlet.airframe.json
1515

16-
trait JSONEventHandler {
17-
def startJson(s: JSONSource, start: Int): Unit
18-
def endJson(s: JSONSource, start: Int): Unit
19-
def startObject(s: JSONSource, start: Int): Unit
20-
def endObject(s: JSONSource, start: Int, end: Int, numElem: Int)
21-
def startArray(s: JSONSource, start: Int): Unit
22-
def endArray(s: JSONSource, start: Int, end: Int, numElem: Int)
16+
trait JSONHandler[Expr] {
17+
def singleContext(s: JSONSource, start: Int): JSONContext[Expr]
18+
def objectContext(s: JSONSource, start: Int): JSONContext[Expr]
19+
def arrayContext(s: JSONSource, start: Int): JSONContext[Expr]
20+
}
21+
22+
/**
23+
* A facade to build json ASTs while scanning json with JSONScanner
24+
* @tparam Expr
25+
*/
26+
trait JSONContext[Expr] extends JSONHandler[Expr] {
27+
def result: Expr
28+
def isObjectContext: Boolean
29+
private[json] final def endScannerState: Int = {
30+
if (isObjectContext) JSONScanner.OBJECT_END
31+
else JSONScanner.ARRAY_END
32+
}
33+
34+
def add(v: Expr): Unit
35+
def closeContext(s: JSONSource, end: Int): Unit
2336

24-
def nullValue(s: JSONSource, start: Int, end: Int): Unit
25-
def stringValue(s: JSONSource, start: Int, end: Int): Unit
26-
def numberValue(s: JSONSource, start: Int, end: Int, dotIndex: Int, expIndex: Int): Unit
27-
def booleanValue(s: JSONSource, v: Boolean, start: Int, end: Int)
37+
def addNull(s: JSONSource, start: Int, end: Int): Unit
38+
def addString(s: JSONSource, start: Int, end: Int): Unit
39+
def addNumber(s: JSONSource, start: Int, end: Int, dotIndex: Int, expIndex: Int): Unit
40+
def addBoolean(s: JSONSource, v: Boolean, start: Int, end: Int)
2841
}

0 commit comments

Comments
 (0)