Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add weejson-jsoniter-scala #105

Open
wants to merge 12 commits into
base: v1
Choose a base branch
from
48 changes: 48 additions & 0 deletions bench/src/main/scala/bench/JsoniterScalaBench.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package bench

import com.rallyhealth.weejson.v1.BufferedValue
import com.rallyhealth.weejson.v1.wee_jsoniter_scala.FromJsoniterScala
import org.openjdk.jmh.annotations._

import java.util.concurrent.TimeUnit

/**
* Quick and dirty test to see how badly we're butchering performance of floats.
*
* ==Quick Run==
* bench / Jmh / run .*JsoniterScalaBench
*
* ==Profile with Flight Recorder==
* bench / Jmh / run -prof jfr .*JsoniterScalaBench
*
* ==Jmh Visualizer Report==
* bench / Jmh / run -prof gc -rf json -rff JsoniterScalaBench-results.json .*JsoniterScalaBench
*
* @see https://github.com/ktoso/sbt-jmh
*/
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 15, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(
jvmArgsAppend =
Array("-Xmx350m", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:-BackgroundCompilation", "-XX:-TieredCompilation"),
value = 1
)
class JsoniterScalaBench {

/**
* Values that end with a number throw an expensive exception internally when reaching EOF.
* The only time this would happen in the wild would be when parsing a JSON text of a single number.
* To make this more realistic, we're intentionally adding a whitespace suffix here.
*/
private val floatBytes = "-3.14159 ".getBytes()
private val intBytes = "186282 ".getBytes()

@Benchmark
def parseFloat = FromJsoniterScala(floatBytes).transform(BufferedValue.Builder)

@Benchmark
def parseInt = FromJsoniterScala(intBytes).transform(BufferedValue.Builder)
}
13 changes: 13 additions & 0 deletions bench/src/test/scala/bench/JsoniterScalaBenchTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package bench

import com.rallyhealth.weejson.v1.BufferedValue._
import utest._

object JsoniterScalaBenchTests extends TestSuite {

val tests = Tests {
val bench = new JsoniterScalaBench()
test("parseFloat")(bench.parseFloat ==> Num("-3.14159", 2, -1))
test("parseInt")(bench.parseInt ==> NumLong(186282))
}
}
23 changes: 23 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ lazy val bench = project
.dependsOn(
`weepickle-tests` % "compile;test",
`weejson-upickle`,
`weejson-jsoniter-scala`,
)
.enablePlugins(JmhPlugin)
.settings(
Expand Down Expand Up @@ -94,6 +95,7 @@ lazy val `weepickle-tests` = project
`weejson-argonaut`,
`weejson-circe`,
`weejson-json4s`,
`weejson-jsoniter-scala`,
`weejson-play-base`,
`weejson` % "compile;test->test",
`weepack` % "compile;test->test",
Expand Down Expand Up @@ -137,6 +139,27 @@ lazy val `weejson-jackson` = project
)
)

/**
* A very fast JSON parser.
*
* Uses MiMa, but not shaded. Package naming strategy across major versions is unknown.
*
* @see https://github.com/plokhotnyuk/jsoniter-scala
*/
lazy val `weejson-jsoniter-scala` = project
.dependsOn(`weepickle-core`)
.settings(
libraryDependencies ++= Seq(
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.12.0"
),
mimaPreviousArtifacts := {
if (VersionNumber(version.value).matchesSemVer(SemanticSelector("<1.8.0")))
Set.empty
else
mimaPreviousArtifacts.value
}
)

lazy val `weejson-circe` = project
.dependsOn(weejson)
.settings(
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.5.5
sbt.version=1.6.2
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.9.2")
addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1")
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.7")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2")
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.rallyhealth.weejson.v1.wee_jsoniter_scala

import com.github.plokhotnyuk.jsoniter_scala.core._
import com.rallyhealth.weejson.v1.wee_jsoniter_scala.WeePickleJsonValueCodecs.VisitorJsonValueEncoder
import com.rallyhealth.weepickle.v1.core.{FromInput, Visitor}

import java.io.InputStream
import java.nio.ByteBuffer

/**
* A very fast UTF-8-only JSON parser.
*
* This integration:
* - tracks paths and returns a JsonPointer on error
* - does not deduplicate string keys (as jackson-core does)
* - throws when a fixed depth limit is reached
*
* @see https://github.com/plokhotnyuk/jsoniter-scala
*/
object FromJsoniterScala extends FromJsoniterScala(
ReaderConfig
.withAppendHexDumpToParseException(false) // avoid leaking sensitive plaintext data
)

class FromJsoniterScala(config: ReaderConfig) {

def apply(
bytes: Array[Byte]
): FromInput = new FromInput {

override def transform[T](
to: Visitor[_, T]
): T = readFromArray(bytes, config)(readerCodec(to))
}

def apply(
in: InputStream
): FromInput = new FromInput {

override def transform[T](
to: Visitor[_, T]
): T = readFromStream(in, config)(readerCodec(to))
}

def apply(
buf: ByteBuffer
): FromInput = new FromInput {

override def transform[T](
to: Visitor[_, T]
): T = readFromByteBuffer(buf, config)(readerCodec(to))
}

private def readerCodec[J](
v: Visitor[_, J]
): JsonValueCodec[J] = new VisitorJsonValueEncoder[J](v, maxDepth = 64)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.rallyhealth.weejson.v1.wee_jsoniter_scala

import com.github.plokhotnyuk.jsoniter_scala.core.JsonWriter
import com.rallyhealth.weepickle.v1.core._

import java.time.Instant

class JsonWriterVisitor(
writer: JsonWriter
) extends JsVisitor[Any, JsonWriter] {

private[this] val arrVisitor = new ArrVisitor[Any, JsonWriter] {
override def subVisitor: Visitor[_, _] = JsonWriterVisitor.this

override def visitValue(
v: Any
): Unit = ()

override def visitEnd(): JsonWriter = {
writer.writeArrayEnd()
writer
}
}

private[this] val objVisitor = new ObjVisitor[Any, JsonWriter] {
writer.writeObjectStart()
override def visitKey(): Visitor[_, _] = StringVisitor

override def visitKeyValue(
v: Any
): Unit = writer.writeKey(v.toString)

override def subVisitor: Visitor[_, _] = JsonWriterVisitor.this

override def visitValue(
v: Any
): Unit = ()

override def visitEnd(): JsonWriter = {
writer.writeObjectEnd()
writer
}
}

override def visitArray(
length: Int
): ArrVisitor[Any, JsonWriter] = arrVisitor

override def visitObject(
length: Int
): ObjVisitor[Any, JsonWriter] = objVisitor

override def visitNull(): JsonWriter = {
writer.writeNull()
writer
}

override def visitFalse(): JsonWriter = {
writer.writeVal(false)
writer
}

override def visitTrue(): JsonWriter = {
writer.writeVal(true)
writer
}

override def visitFloat64StringParts(
cs: CharSequence,
decIndex: Int,
expIndex: Int
): JsonWriter = {
writer.writeNonEscapedAsciiVal(cs.toString)
writer
}

override def visitFloat64(
d: Double
): JsonWriter = {
writer.writeVal(d)
writer
}

override def visitFloat32(
d: Float
): JsonWriter = {
writer.writeVal(d)
writer
}

override def visitInt32(
i: Int
): JsonWriter = {
writer.writeVal(i)
writer
}

override def visitInt64(
l: Long
): JsonWriter = {
writer.writeVal(l)
writer
}

override def visitFloat64String(
s: String
): JsonWriter = {
writer.writeNonEscapedAsciiVal(s)
writer
}

override def visitString(
cs: CharSequence
): JsonWriter = {
writer.writeVal(cs.toString)
writer
}

override def visitChar(
c: Char
): JsonWriter = {
writer.writeVal(c)
writer
}

override def visitTimestamp(
instant: Instant
): JsonWriter = {
writer.writeVal(instant)
writer
}

override def visitBinary(
bytes: Array[Byte],
offset: Int,
len: Int
): JsonWriter = {
val trimmed =
if (offset == 0 && bytes.length <= len) bytes
else bytes.slice(offset, offset + len)

writer.writeBase64Val(trimmed, true)
writer
}
}
Loading