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

NativeJsonInput/Output with custom format #610

Merged
merged 6 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions benchmark/js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
How to run benchmark:
- compile `sbt commons-benchmark-js/fullOptJS`
- open `fullopt-2.13.html` file in a browser
- select test suite and run
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.avsystem.commons
package ser

import com.avsystem.commons.serialization.json.{JsonStringInput, JsonStringOutput}
import com.avsystem.commons.serialization.nativejs.{NativeJsonInput, NativeJsonOutput}
import io.circe.parser._
import io.circe.syntax._
import japgolly.scalajs.benchmark.gui.GuiSuite
Expand All @@ -10,62 +11,86 @@ import japgolly.scalajs.benchmark.{Benchmark, Suite}
object JsonBenchmarks {
val suite = GuiSuite(
Suite("JSON serialization benchmarks")(
Benchmark("Writing case class: GenCodec") {
Benchmark("Writing case class: GenCodec, String Json format") {
JsonStringOutput.write(Something.Example)
},
Benchmark("Writing case class: GenCodec, Native Json format") {
NativeJsonOutput.writeAsString(Something.Example)
},
Benchmark("Writing case class: Circe") {
Something.Example.asJson.noSpaces
},
Benchmark("Writing case class: uPickle") {
upickle.default.write(Something.Example)
},
Benchmark("Reading case class: GenCodec") {
Benchmark("Reading case class: GenCodec, String Json format") {
JsonStringInput.read[Something](Something.ExampleJsonString)
},
Benchmark("Reading case class: GenCodec, Native Json format") {
NativeJsonInput.readString[Something](Something.ExampleJsonString)
},
Benchmark("Reading case class: Circe") {
decode[Something](Something.ExampleJsonString).fold(e => throw e, identity)
},
Benchmark("Reading case class: uPickle") {
upickle.default.read[Something](Something.ExampleJsonString)
},

Benchmark("Writing sealed hierarchy: GenCodec") {
Benchmark("Writing sealed hierarchy: GenCodec, String Json format") {
JsonStringOutput.write(SealedStuff.ExampleList)
},
Benchmark("Writing sealed hierarchy: GenCodec (flat)") {
Benchmark("Writing sealed hierarchy: GenCodec (flat), String Json format") {
JsonStringOutput.write(FlatSealedStuff.ExampleList)
},
Benchmark("Writing sealed hierarchy: GenCodec, Native Json format") {
NativeJsonOutput.writeAsString(SealedStuff.ExampleList)
},
Benchmark("Writing sealed hierarchy: GenCodec (flat), Native Json format") {
NativeJsonOutput.writeAsString(FlatSealedStuff.ExampleList)
},
Benchmark("Writing sealed hierarchy: Circe") {
SealedStuff.ExampleList.asJson.noSpaces
},
Benchmark("Writing sealed hierarchy: uPickle") {
upickle.default.write(SealedStuff.ExampleList)
},
Benchmark("Reading sealed hierarchy: GenCodec") {
Benchmark("Reading sealed hierarchy: GenCodec, String Json format") {
JsonStringInput.read[List[SealedStuff]](SealedStuff.ExampleJsonString)
},
Benchmark("Reading sealed hierarchy: GenCodec (flat)") {
Benchmark("Reading sealed hierarchy: GenCodec (flat), String Json format") {
JsonStringInput.read[List[FlatSealedStuff]](FlatSealedStuff.ExampleJsonString)
},
Benchmark("Reading sealed hierarchy: GenCodec, Native Json format") {
NativeJsonInput.readString[List[SealedStuff]](SealedStuff.ExampleJsonString)
},
Benchmark("Reading sealed hierarchy: GenCodec (flat), Native Json format") {
NativeJsonInput.readString[List[FlatSealedStuff]](FlatSealedStuff.ExampleJsonString)
},
Benchmark("Reading sealed hierarchy: Circe") {
decode[List[SealedStuff]](SealedStuff.ExampleJsonString).fold(e => throw e, identity)
},
Benchmark("Reading sealed hierarchy: uPickle") {
upickle.default.read[List[SealedStuff]](SealedStuff.ExampleUpickleJsonString)
},

Benchmark("Writing foos: GenCodec") {
Benchmark("Writing foos: GenCodec, String Json format") {
JsonStringOutput.write(Foo.ExampleMap)
},
Benchmark("Writing foos: GenCodec, Native Json format") {
NativeJsonOutput.writeAsString(Foo.ExampleMap)
},
Benchmark("Writing foos: Circe") {
Foo.ExampleMap.asJson.noSpaces
},
Benchmark("Writing foos: uPickle") {
upickle.default.write(Foo.ExampleMap)
},
Benchmark("Reading foos: GenCodec") {
Benchmark("Reading foos: GenCodec, String Json format") {
JsonStringInput.read[Map[String, Foo]](Foo.ExampleJsonString)
},
Benchmark("Reading foos: GenCodec with Native Json format") {
NativeJsonInput.readString[Map[String, Foo]](Foo.ExampleJsonString)
},
Benchmark("Reading foos: Circe") {
decode[Map[String, Foo]](Foo.ExampleJsonString).fold(e => throw e, identity)
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.avsystem.commons
package serialization.nativejs

import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx}

/**
* Specifies format used by `NativeJsonOutput.writeLong` / `NativeJsonInput.readLong`
* to represent [[Long]]. JS does not support 64-bit representation.
*/
final class NativeLongFormat(implicit ctx: EnumCtx) extends AbstractValueEnum
object NativeLongFormat extends AbstractValueEnumCompanion[NativeLongFormat] {
final val RawString: Value = new NativeLongFormat
final val JsNumber: Value = new NativeLongFormat
final val JsBigInt: Value = new NativeLongFormat
}

/**
* Specifies format used by `NativeJsonOutput.writeTimestamp` / `NativeJsonInput.readTimestamp`
* to represent timestamps.
*/
final class NativeDateFormat(implicit ctx: EnumCtx) extends AbstractValueEnum
object NativeDateFormat extends AbstractValueEnumCompanion[NativeDateFormat] {
final val RawString: Value = new NativeDateFormat
final val JsNumber: Value = new NativeDateFormat
final val JsDate: Value = new NativeDateFormat
}

ddworak marked this conversation as resolved.
Show resolved Hide resolved
/**
* Specifies format used by `NativeJsonOutput.writeBigInt` / `NativeJsonInput.readBigInt`
* to represent [[BigInt]].
*
* Note that [[scala.scalajs.js.JSON.stringify]] does not know how to serialize a BigInt and throws an error
*/
final class NativeBigIntFormat(implicit ctx: EnumCtx) extends AbstractValueEnum
object NativeBigIntFormat extends AbstractValueEnumCompanion[NativeBigIntFormat] {
final val RawString: Value = new NativeBigIntFormat
final val JsBigInt: Value = new NativeBigIntFormat
}

/**
* Adjusts format produced by [[NativeJsonOutput]].
*
* @param longFormat format used to [[Long]]
* @param dateFormat format used to represent timestamps
* @param bigIntFormat format used to represent [[BigInt]]
*/
final case class NativeFormatOptions(
longFormat: NativeLongFormat = NativeLongFormat.RawString,
dateFormat: NativeDateFormat = NativeDateFormat.RawString,
bigIntFormat: NativeBigIntFormat = NativeBigIntFormat.RawString,
)
object NativeFormatOptions {
final val RawString = NativeFormatOptions()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package com.avsystem.commons
package serialization.nativejs

import com.avsystem.commons.annotation.explicitGenerics
import com.avsystem.commons.serialization.GenCodec.ReadFailure
import com.avsystem.commons.serialization.*
import com.avsystem.commons.serialization.json.RawJson

import scala.scalajs.js
import scala.scalajs.js.JSON

class NativeJsonInput(value: js.Any, options: NativeFormatOptions) extends InputAndSimpleInput { self =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extend com.avsystem.commons.ser.JsonBenchmarks (these are JS benchmarks for serialization) and add results to this MR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What com.avsystem.commons.serialization.nativejs.NativeFormatOptions should I use for the benchmark?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The defaults. You can also add a separate benchmark for choosing them if you'd like to make a decision based on performance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Results, Native is almost 2 times faster that standard Gencodec
Screenshot from 2024-08-05 13-05-26

private def read[T](expected: String)(matcher: PartialFunction[Any, T]): T =
matcher.applyOrElse(value, (o: Any) => throw new ReadFailure(s"Cannot read $expected, got: ${js.typeOf(o)}"))

override def readNull(): Boolean =
value == null

override def readString(): String =
read("String") {
case s: String => s
}

override def readDouble(): Double =
read("Double") {
case v: Double => v
}

override def readInt(): Int =
read("Int") {
case v: Int => v
}

override def readLong(): Long = {
def fromString(s: String): Long =
try s.toLong
catch {
case e: NumberFormatException => throw new ReadFailure(s"Cannot read Long", e)
}
read("Long") {
case s: String => fromString(s)
case i: Int => i
case d: Double if d.isWhole => d.toLong
case b: js.BigInt => fromString(b.toString)
// for some reason pattern match on js.BigInt type does not seem to work, check type manually
case b if js.typeOf(b) == "bigint" => fromString(b.asInstanceOf[js.BigInt].toString)
}
}

override def readBigInt(): BigInt = {
def fromString(s: String): BigInt =
try BigInt(s)
catch {
case e: NumberFormatException => throw new ReadFailure(s"Cannot read BigInt", e)
}

read("BigInt") {
case s: String => fromString(s)
case i: Int => BigInt(i)
case d: Double if d.isWhole => BigInt(d.toLong)
case b: js.BigInt => fromString(b.toString)
// for some reason pattern match on js.BigInt type does not seem to work, check type manually
case b if js.typeOf(b) == "bigint" => fromString(b.asInstanceOf[js.BigInt].toString)
}
}

override def readBigDecimal(): BigDecimal = {
def fromString(s: String): BigDecimal =
try BigDecimal(s)
catch {
case e: NumberFormatException => throw new ReadFailure(s"Cannot read BigDecimal", e)
}
read("BigDecimal") {
case s: String => fromString(s)
case i: Int => BigDecimal(i)
case d: Double => BigDecimal(d)
}
}

override def readBoolean(): Boolean =
read("Boolean") {
case v: Boolean => v
}

override def readList(): ListInput =
read("List") {
case array: js.Array[js.Any @unchecked] => new NativeJsonListInput(array, options)
}

override def readObject(): ObjectInput =
read("Object") {
case obj: js.Object => new NativeJsonObjectInput(obj.asInstanceOf[js.Dictionary[js.Any]], options)
}

override def readTimestamp(): Long = options.dateFormat match {
case NativeDateFormat.RawString | NativeDateFormat.JsNumber =>
readLong() // lenient behaviour, accept any value that can be interpreted as Long
case NativeDateFormat.JsDate =>
read("js.Date") {
case v: js.Date => v.getTime().toLong
}
}

override def skip(): Unit = ()

override def readBinary(): Array[Byte] =
read("Binary") {
case array: js.Array[Int @unchecked] => array.iterator.map(_.toByte).toArray
}

override def readCustom[T](typeMarker: TypeMarker[T]): Opt[T] =
typeMarker match {
case RawJson => JSON.stringify(readRaw()).opt
case _ => Opt.Empty
}

def readRaw(): js.Any = value
}

final class NativeJsonListInput(array: js.Array[js.Any], options: NativeFormatOptions) extends ListInput {
private var it = 0

override def hasNext: Boolean =
it < array.length

override def nextElement(): Input = {
val in = new NativeJsonInput(array(it), options)
it += 1
in
}
}

final class NativeJsonObjectInput(dict: js.Dictionary[js.Any], options: NativeFormatOptions) extends ObjectInput {
private val it = dict.iterator

override def hasNext: Boolean =
it.hasNext

override def peekField(name: String): Opt[FieldInput] =
if (dict.contains(name)) Opt(new NativeJsonFieldInput(name, dict(name), options)) else Opt.Empty

override def nextField(): FieldInput = {
val (key, value) = it.next()
new NativeJsonFieldInput(key, value, options)
}
}

final class NativeJsonFieldInput(
val fieldName: String,
value: js.Any,
options: NativeFormatOptions,
) extends NativeJsonInput(value, options)
with FieldInput

object NativeJsonInput {
@explicitGenerics
def read[T: GenCodec](value: js.Any, options: NativeFormatOptions = NativeFormatOptions.RawString): T =
GenCodec.read[T](new NativeJsonInput(value, options))

@explicitGenerics
def readString[T: GenCodec](value: String, options: NativeFormatOptions = NativeFormatOptions.RawString): T =
ddworak marked this conversation as resolved.
Show resolved Hide resolved
read[T](JSON.parse(value), options)
}
Loading
Loading