Skip to content
This repository has been archived by the owner on Sep 27, 2022. It is now read-only.

Add jackson-cbor, jackson-smile, jsoniter-scala, and kryo-macros #8

Closed
wants to merge 8 commits into from
Closed
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@
A source code for the article "Scala Serialization" at [medium](https://medium.com/@dkomanov/scala-serialization-419d175c888a).

Recent charts for the article is at https://dkomanov.github.io/scala-serialization/.

To build and run benchmarks use the following command:

```sbt
sbt clean 'scala-serialization-test/jmh:run -prof gc -rf json -rff jmh-result.json .*'
```
59 changes: 59 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
lazy val commonSettings = Seq(
organization := "com.komanov",
organizationHomepage := Some(url("https://komanov.com")),
homepage := Some(url("https://dkomanov.github.io/scala-serialization/")),
licenses := Seq(("MIT License", url("https://opensource.org/licenses/mit-license.html"))),
startYear := Some(2016),
scalaVersion := "2.11.12",
resolvers += Resolver.bintrayRepo("evolutiongaming", "maven"),
scalacOptions ++= Seq(
"-deprecation",
"-feature",
"-Xmax-classfile-name", "240",
//"-Xmacro-settings:print-serializers" //to log sources of serializaer which are generated by kryo-macros
//"-Xmacro-settings:print-codecs" //to log sources of codecs which are generated by jsoniter-scala
)
)

lazy val `scala-serialization-all` = project.in(file("."))
.aggregate(`scala-serialization`, `scala-serialization-test`)

lazy val `scala-serialization` = project
.settings(commonSettings: _*)
.settings(
fork in Test := true,
libraryDependencies ++= Seq(
"com.twitter" %% "util-core" % "18.3.0",
"commons-io" % "commons-io" % "2.6",
"com.evolutiongaming" %% "kryo-macros" % "1.1.8",
"com.github.plokhotnyuk.jsoniter-scala" %% "macros" % "0.22.2",
"com.fasterxml.jackson.core" % "jackson-databind" % "2.9.5",
"com.fasterxml.jackson.core" % "jackson-core" % "2.9.5",
"com.fasterxml.jackson.dataformat" % "jackson-dataformat-cbor" % "2.9.5",
"com.fasterxml.jackson.dataformat" % "jackson-dataformat-smile" % "2.9.5",
"com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.9.5",
"com.google.protobuf" % "protobuf-java" % "3.0.0",
"com.trueaccord.scalapb" %% "scalapb-runtime" % "0.5.45",
"org.scala-lang.modules" %% "scala-pickling" % "0.11.0-M2",
"io.suzaku" %% "boopickle" % "1.2.6",
"com.twitter" %% "chill" % "0.9.2",
"com.esotericsoftware" % "kryo-shaded" % "4.0.1", //for chill (should match with version for kryo-macros dependencies)
"org.apache.thrift" % "libthrift" % "0.11.0",
"org.slf4j" % "slf4j-simple" % "1.7.21", //for thrift
"com.twitter" %% "scrooge-core" % "18.3.0",
"org.specs2" %% "specs2-core" % "3.8.3" % Test,
"org.specs2" %% "specs2-matcher-extra" % "3.8.3" % Test,
"org.specs2" %% "specs2-mock" % "3.8.3" % Test,
"org.specs2" %% "specs2-junit" % "3.8.3" % Test
)
)

lazy val `scala-serialization-test` = project
.dependsOn(`scala-serialization`)
.enablePlugins(JmhPlugin)
.settings(commonSettings: _*)
.settings(
libraryDependencies ++= Seq(
"pl.project13.scala" % "sbt-jmh-extras" % "0.3.3"
)
)
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jmh.version>1.12</jmh.version>
<jmh.version>1.20</jmh.version>
<jmh.generator>default</jmh.generator>
<javac.target>1.8</javac.target>
<uberjar.name>benchmarks</uberjar.name>
Expand All @@ -26,6 +26,11 @@
<name>Scala-tools Maven2 Repository</name>
<url>http://scala-tools.org/repo-releases</url>
</repository>
<repository>
<id>bintray-evolutiongaming-maven</id>
<name>EvolutionGaming Bintray Repository</name>
<url>https://dl.bintray.com/evolutiongaming/maven</url>
</repository>
</repositories>

<pluginRepositories>
Expand Down
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.1.1
2 changes: 2 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.3")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.0")
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.komanov.serialization.converters._
import com.komanov.serialization.domain.{EventProcessor, Site, SiteEvent}
import org.openjdk.jmh.annotations._

@State(Scope.Benchmark)
@State(Scope.Thread) // Kryo modifies bytes during parsing, see: https://github.com/EsotericSoftware/kryo#threading
@BenchmarkMode(Array(Mode.AverageTime))
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(value = 1, jvmArgs = Array("-Xmx2G"))
Expand Down Expand Up @@ -229,7 +229,15 @@ abstract class BenchmarkBase(converter: MyConverter) {

}

class JsonBenchmark extends BenchmarkBase(JsonConverter)
class KryoMacrosBenchmark extends BenchmarkBase(KryoMacrosConverter)

class JsoniterScalaBenchmark extends BenchmarkBase(JsoniterScalaConverter)

class JacksonCborBenchmark extends BenchmarkBase(JacksonCborConverter)

class JacksonSmileBenchmark extends BenchmarkBase(JacksonSmileConverter)

class JacksonJsonBenchmark extends BenchmarkBase(JacksonJsonConverter)

class ScalaPbBenchmark extends BenchmarkBase(ScalaPbConverter)

Expand Down
47 changes: 33 additions & 14 deletions scala-serialization/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,63 +18,82 @@
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.11.7</version>
<version>2.11.12</version>
</dependency>
<dependency>
<groupId>com.twitter</groupId>
<artifactId>util-core_2.11</artifactId>
<version>6.34.0</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
<version>2.6</version>
</dependency>
<dependency>
<groupId>com.evolutiongaming</groupId>
<artifactId>kryo-macros_2.11</artifactId>
<version>1.1.8</version>
</dependency>
<dependency>
<groupId>com.github.plokhotnyuk.jsoniter-scala</groupId>
<artifactId>macros_2.11</artifactId>
<version>0.22.2</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.3</version>
<version>2.9.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.7.3</version>
<version>2.9.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-cbor</artifactId>
<version>2.9.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-smile</artifactId>
<version>2.9.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-scala_2.11</artifactId>
<version>2.7.3</version>
<version>2.9.5</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.0.0-beta-2</version>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.trueaccord.scalapb</groupId>
<artifactId>scalapb-runtime_2.11</artifactId>
<version>0.5.31</version>
<version>0.5.45</version>
</dependency>
<dependency>
<groupId>org.scala-lang.modules</groupId>
<artifactId>scala-pickling_2.11</artifactId>
<version>0.11.0-M2</version>
</dependency>
<dependency>
<groupId>me.chrons</groupId>
<groupId>io.suzaku</groupId>
<artifactId>boopickle_2.11</artifactId>
<version>1.2.4</version>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>com.twitter</groupId>
<artifactId>chill_2.11</artifactId>
<version>0.8.0</version>
<version>0.9.2</version>
</dependency>
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.9.1</version>
<version>0.11.0</version>
</dependency>
<dependency>
<!-- for thrift -->
Expand All @@ -85,7 +104,7 @@
<dependency>
<groupId>com.twitter</groupId>
<artifactId>scrooge-core_2.11</artifactId>
<version>4.7.0</version>
<version>18.3.0</version>
</dependency>

<!-- test -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@ package com.komanov.serialization.converters

import java.time.Instant

import com.fasterxml.jackson.core.{JsonGenerator, JsonParser, Version}
import com.fasterxml.jackson.core.{JsonFactory, JsonGenerator, JsonParser, Version}
import com.fasterxml.jackson.databind.Module.SetupContext
import com.fasterxml.jackson.databind._
import com.fasterxml.jackson.databind.module.{SimpleDeserializers, SimpleSerializers}
import com.fasterxml.jackson.dataformat.cbor.{CBORFactory, CBORGenerator}
import com.fasterxml.jackson.dataformat.smile.SmileFactory
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.komanov.serialization.domain.{Site, SiteEvent, SiteEventData}
import com.komanov.serialization.domain.{Site, SiteEvent}

/** https://github.com/FasterXML/jackson */
object JsonConverter extends MyConverter {
object JacksonJsonConverter extends JacksonConverter(new JsonFactory())

object JacksonCborConverter extends JacksonConverter(new CBORFactory().disable(CBORGenerator.Feature.WRITE_MINIMAL_INTS))

object JacksonSmileConverter extends JacksonConverter(new SmileFactory())

abstract class JacksonConverter(jsonFactory: JsonFactory) extends MyConverter {
private object InstantModule extends Module {
override def getModuleName: String = "Instant"

Expand All @@ -38,7 +45,7 @@ object JsonConverter extends MyConverter {
}

private val objectMapper = {
val om = new ObjectMapper()
val om = new ObjectMapper(jsonFactory)
om.registerModule(new DefaultScalaModule)
om.registerModule(InstantModule)
om
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.komanov.serialization.converters

import java.time.Instant

import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros._
import com.komanov.serialization.domain.{Site, SiteEvent}

/** https://github.com/plokhotnyuk/jsoniter-scala */
object JsoniterScalaConverter extends MyConverter {
private[this] val writerConfig = WriterConfig(preferredBufSize = 131072)
Copy link
Owner

Choose a reason for hiding this comment

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

Such size of a buffer seems like a hack. Other libraries do reallocations during serialization/deserialization.

Copy link
Author

@plokhotnyuk plokhotnyuk Mar 24, 2018

Choose a reason for hiding this comment

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

It is not a hack, it is using a provided feature for a competitive advantage)

Copy link

Choose a reason for hiding this comment

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

@plokhotnyuk that's benchmarks-aware ad hocs
@dkomanov maybe you need to add 1Mb records ;)

Copy link
Author

Choose a reason for hiding this comment

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

Using of preallocated thread-local buffers with the capacity that is enough for expected workload is a normal practice for any parformance-aware parser/serializer.

BTW, jsoniter-scala is able to serialize huge messages efficiently using ~32K buffers in the writeToStream routine:
https://github.com/plokhotnyuk/jsoniter-scala/blob/master/jsoniter-scala-core/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/package.scala#L190

private[this] val readerConfig = ReaderConfig(preferredBufSize = 131072, preferredCharBufSize = 131072)
private[this] implicit val instantCodec: JsonValueCodec[Instant] = new JsonValueCodec[Instant] {
override def nullValue: Instant = null

override def decodeValue(in: JsonReader, default: Instant): Instant = Instant.ofEpochMilli(in.readLong())

override def encodeValue(x: Instant, out: JsonWriter): Unit = out.writeVal(x.toEpochMilli)
}
private[this] implicit val siteCodec: JsonValueCodec[Site] = JsonCodecMaker.make[Site](CodecMakerConfig())
private[this] implicit val siteEventCodec: JsonValueCodec[SiteEvent] = JsonCodecMaker.make[SiteEvent](CodecMakerConfig())

def toByteArray(site: Site): Array[Byte] = writeToArray(site, writerConfig)

def fromByteArray(bytes: Array[Byte]): Site = readFromArray[Site](bytes, readerConfig)

def toByteArray(event: SiteEvent): Array[Byte] = writeToArray(event, writerConfig)

def siteEventFromByteArray(clazz: Class[_], bytes: Array[Byte]): SiteEvent = readFromArray[SiteEvent](bytes, readerConfig)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.komanov.serialization.converters

import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.io.{FastInput, FastOutput}
import com.evolutiongaming.kryo.Serializer
import com.komanov.serialization.domain._

/** https://github.com/evolution-gaming/kryo-macros */
case class KryoMacrosConverter(kryo: Kryo, in: FastInput, out: FastOutput) {
def read[A <: AnyRef](bs: Array[Byte])(implicit s: com.esotericsoftware.kryo.Serializer[A], m: Manifest[A]): A = {
in.setBuffer(bs)
kryo.readObject(in, m.runtimeClass.asInstanceOf[Class[A]], s)
}

def write[A <: AnyRef](a: A)(implicit s: com.esotericsoftware.kryo.Serializer[A]): Array[Byte] = {
out.clear()
kryo.writeObject(out, a, s)
out.toBytes
}
}

object KryoMacrosConverter extends MyConverter {
private[this] val pool = new ThreadLocal[KryoMacrosConverter] {
override def initialValue(): KryoMacrosConverter =
KryoMacrosConverter(new Kryo, new FastInput(), new FastOutput(131072, 131072))
}
private[this] implicit val domainSerializer = Serializer.make[Domain]
private[this] implicit val metaTagSerializer = Serializer.make[MetaTag]
private[this] implicit val entryPointSerializer = Serializer.makeCommon[EntryPoint] {
case 0 => Serializer.inner[DomainEntryPoint]
case 1 => Serializer.inner[FreeEntryPoint]
}
private[this] implicit val pageComponentDataSerializer = Serializer.makeCommon[PageComponentData] {
case 0 => Serializer.inner[TextComponentData]
case 1 => Serializer.inner[ButtonComponentData]
case 2 => Serializer.inner[BlogComponentData]
}
private[this] implicit val pageComponentPositionSerializer = Serializer.make[PageComponentPosition]
private[this] implicit val pageComponentSerializer = Serializer.make[PageComponent]
private[this] implicit val pageSerializer = Serializer.make[Page]
private[this] implicit val siteSerializer = Serializer.make[Site]
private[this] implicit val siteEventSerializer = Serializer.makeCommon[SiteEvent] {
case 0 => Serializer.inner[SiteCreated]
case 1 => Serializer.inner[SiteNameSet]
case 2 => Serializer.inner[SiteDescriptionSet]
case 3 => Serializer.inner[SiteRevisionSet]
case 4 => Serializer.inner[SitePublished]
case 5 => Serializer.inner[SiteUnpublished]
case 6 => Serializer.inner[SiteFlagAdded]
case 7 => Serializer.inner[SiteFlagRemoved]
case 8 => Serializer.inner[DomainAdded]
case 9 => Serializer.inner[DomainRemoved]
case 10 => Serializer.inner[PrimaryDomainSet]
case 11 => Serializer.inner[DefaultMetaTagAdded]
case 12 => Serializer.inner[DefaultMetaTagRemoved]
case 13 => Serializer.inner[PageAdded]
case 14 => Serializer.inner[PageRemoved]
case 15 => Serializer.inner[PageNameSet]
case 16 => Serializer.inner[PageMetaTagAdded]
case 17 => Serializer.inner[PageMetaTagRemoved]
case 18 => Serializer.inner[PageComponentAdded]
case 19 => Serializer.inner[PageComponentRemoved]
case 20 => Serializer.inner[PageComponentPositionSet]
case 21 => Serializer.inner[PageComponentPositionReset]
case 22 => Serializer.inner[TextComponentDataSet]
case 23 => Serializer.inner[ButtonComponentDataSet]
case 24 => Serializer.inner[BlogComponentDataSet]
case 25 => Serializer.inner[DomainEntryPointAdded]
case 26 => Serializer.inner[FreeEntryPointAdded]
case 27 => Serializer.inner[EntryPointRemoved]
case 28 => Serializer.inner[PrimaryEntryPointSet]
}

def toByteArray(site: Site): Array[Byte] = pool.get().write(site)

def fromByteArray(bytes: Array[Byte]): Site = pool.get().read[Site](bytes)

def toByteArray(event: SiteEvent): Array[Byte] = pool.get().write(event)

def siteEventFromByteArray(clazz: Class[_], bytes: Array[Byte]): SiteEvent = pool.get().read[SiteEvent](bytes)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package com.komanov.serialization.converters
object Converters {

val all: Seq[(String, MyConverter)] = Seq(
"JSON" -> JsonConverter,
"KryoMacros" -> KryoMacrosConverter,
"JsoniterScala" -> JsoniterScalaConverter,
"Jackson CBOR" -> JacksonCborConverter,
"Jackson JSON" -> JacksonJsonConverter,
"Jackson Smile" -> JacksonSmileConverter,
"ScalaPB" -> ScalaPbConverter,
"Java PB" -> JavaPbConverter,
"Java Thrift" -> JavaThriftConverter,
Expand Down
Loading