Skip to content

Commit

Permalink
Optimize object merging for common cases used in Json macros (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmethvin authored and marcospereira committed Jun 3, 2017
1 parent 8642c48 commit 651b57e
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 7 deletions.
9 changes: 9 additions & 0 deletions benchmarks/build.sbt
@@ -0,0 +1,9 @@

import pl.project13.scala.sbt.JmhPlugin

sourceDirectory in Jmh := (sourceDirectory in Test).value
classDirectory in Jmh := (classDirectory in Test).value
dependencyClasspath in Jmh := (dependencyClasspath in Test).value
// rewire tasks, so that 'jmh:run' automatically invokes 'jmh:compile' (otherwise a clean 'jmh:run' would fail)
compile in Jmh <<= (compile in Jmh) dependsOn (compile in Test)
run in Jmh <<= (run in Jmh) dependsOn (Keys.compile in Jmh)
29 changes: 29 additions & 0 deletions benchmarks/src/test/scala/play/api/libs/json/Employee.scala
@@ -0,0 +1,29 @@
package play.api.libs.json

case class Employee(
employeeNumber: Int,
firstName: String,
lastName: String,
city: String,
country: String,
tags: Seq[String]
)

object Employee {
implicit val employeeFormat: Format[Employee] = Json.format[Employee]

val manualWrites: Writes[Employee] = Writes { e =>
Json.obj(
"employeeNumber" -> e.employeeNumber,
"firstName" -> e.firstName,
"lastName" -> e.lastName,
"city" -> e.city,
"country" -> e.country,
"tags" -> JsArray(e.tags.map(JsString.apply))
)
}

val manualSeqWrites: Writes[Seq[Employee]] = Writes { seq =>
JsArray(seq.map(manualWrites.writes))
}
}
@@ -0,0 +1,52 @@
/**
* Copyright (C) 2009-2017 Lightbend Inc. <https://www.lightbend.com>
*/
package play.api.libs.json

import org.openjdk.jmh.annotations._

@State(Scope.Benchmark)
class JsonMacros_01_SerializeSimpleCaseClass {

var employee: Employee = _
var employeeJson: JsValue = _

@Setup(Level.Iteration)
def setup(): Unit = {
employee = Employee(
42,
"Foo",
"Bar",
"New York",
"United States",
Seq("engineering", "new", "bar")
)
}

@TearDown(Level.Iteration)
def tearDown(): Unit = {
assert(employeeJson == Json.parse(
""" {
| "employeeNumber": 42,
| "firstName": "Foo",
| "lastName": "Bar",
| "city": "New York",
| "country": "United States",
| "tags": ["engineering", "new", "bar"]
| }
""".stripMargin))
}

@Benchmark
def toJson(): JsValue = {
employeeJson = Json.toJson(employee)
employeeJson
}

@Benchmark
def toJsonManualWrites(): JsValue = {
employeeJson = Json.toJson(employee)(Employee.manualWrites)
employeeJson
}

}
@@ -0,0 +1,41 @@
package play.api.libs.json

import org.openjdk.jmh.annotations._

@State(Scope.Benchmark)
class JsonMacros_02_SerializeList {

var employees: Seq[Employee] = _
var employeesJson: JsValue = _

@Setup(Level.Iteration)
def setup(): Unit = {
employees = (1 to 100) map { id =>
Employee(
id,
s"Foo$id",
s"Bar$id",
"New York",
"United States",
Seq("a", "b", "c")
)
}
}

@TearDown(Level.Iteration)
def tearDown(): Unit = {

}

@Benchmark
def toJson(): JsValue = {
employeesJson = Json.toJson(employees)
employeesJson
}

@Benchmark
def toJsonManualWrites(): JsValue = {
employeesJson = Json.toJson(employees)(Employee.manualSeqWrites)
employeesJson
}
}
7 changes: 7 additions & 0 deletions build.sbt
Expand Up @@ -160,6 +160,13 @@ lazy val `play-functional` = crossProject.crossType(CrossType.Pure)
lazy val `play-functionalJVM` = `play-functional`.jvm
lazy val `play-functionalJS` = `play-functional`.js

import pl.project13.scala.sbt.JmhPlugin
lazy val benchmarks = project
.in(file("benchmarks"))
.enablePlugins(JmhPlugin, PlayNoPublish)
.settings(commonSettings)
.dependsOn(`play-jsonJVM`)

playBuildRepoName in ThisBuild := "play-json"

releaseProcess := Seq[ReleaseStep](
Expand Down
21 changes: 17 additions & 4 deletions play-json/shared/src/main/scala/JsPath.scala
Expand Up @@ -134,7 +134,7 @@ case class IdxPathNode(idx: Int) extends PathNode {
*/
object JsPath extends JsPath(List.empty) {
// TODO implement it correctly (doesn't merge )
def createObj(pathValues: (JsPath, JsValue)*) = {
def createObj(pathValues: (JsPath, JsValue)*): JsObject = {

def buildSubPath(path: JsPath, value: JsValue) = {
def step(path: List[PathNode], value: JsValue): JsObject = {
Expand All @@ -157,9 +157,22 @@ object JsPath extends JsPath(List.empty) {
step(path.path, value)
}

pathValues.foldLeft(JsObject.empty) {
case (obj, (path, value)) =>
obj.deepMerge(buildSubPath(path, value))
// optimize fast path
val objectMap = new scala.collection.mutable.LinkedHashMap[String, JsValue]()
val isSimpleObject = pathValues.forall {
case (JsPath(KeyPathNode(key) :: Nil), value) =>
objectMap.put(key, value)
true
case _ =>
false
}
if (isSimpleObject) {
JsObject(objectMap)
} else {
pathValues.foldLeft(JsObject.empty) {
case (obj, (path, value)) =>
obj.deepMerge(buildSubPath(path, value))
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion play-json/shared/src/main/scala/JsValue.scala
Expand Up @@ -121,7 +121,7 @@ object JsArray extends scala.runtime.AbstractFunction1[IndexedSeq[JsValue], JsAr
* Represent a Json object value.
*/
case class JsObject(
private val underlying: Map[String, JsValue]
private[json] val underlying: Map[String, JsValue]
) extends JsValue {

/**
Expand Down
44 changes: 42 additions & 2 deletions play-json/shared/src/main/scala/Writes.scala
Expand Up @@ -55,10 +55,50 @@ trait OWrites[-A] extends Writes[A] {
object OWrites extends PathWrites with ConstraintWrites {
import play.api.libs.functional._

implicit val functionalCanBuildOWrites: FunctionalCanBuild[OWrites] = new FunctionalCanBuild[OWrites] {
/**
* An `OWrites` merging the results of two separate `OWrites`.
*/
private object MergedOWrites {
def apply[A, B](wa: OWrites[A], wb: OWrites[B]): OWrites[A ~ B] =
new OWritesFromFields[A ~ B] {
def writeFields(fieldsMap: mutable.Map[String, JsValue], obj: A ~ B): Unit = {
val a ~ b = obj
mergeIn(fieldsMap, wa, a)
mergeIn(fieldsMap, wb, b)
}
}

@inline final def mergeIn[A](fieldsMap: mutable.Map[String, JsValue], wa: Writes[A], a: A): Unit = wa match {
case wff: OWritesFromFields[A] =>
wff.writeFields(fieldsMap, a)
case w: OWrites[A] =>
w.writes(a).underlying.foreach {
case (key, value: JsObject) =>
fieldsMap.put(key, fieldsMap.get(key) match {
case Some(o: JsObject) => o deepMerge value
case _ => value
})
case (key, value) =>
fieldsMap.put(key, value)
}
}
}

def apply[A, B](wa: OWrites[A], wb: OWrites[B]): OWrites[A ~ B] = OWrites[A ~ B] { case a ~ b => wa.writes(a).deepMerge(wb.writes(b)) }
/**
* An `OWrites` capable of writing an object incrementally to a mutable map
*/
private trait OWritesFromFields[A] extends OWrites[A] {
def writeFields(fieldsMap: mutable.Map[String, JsValue], a: A)

def writes(a: A): JsObject = {
val fieldsMap = new mutable.LinkedHashMap[String, JsValue]()
writeFields(fieldsMap, a)
JsObject(fieldsMap)
}
}

implicit val functionalCanBuildOWrites: FunctionalCanBuild[OWrites] = new FunctionalCanBuild[OWrites] {
def apply[A, B](wa: OWrites[A], wb: OWrites[B]): OWrites[A ~ B] = MergedOWrites[A, B](wa, wb)
}

implicit val contravariantfunctorOWrites: ContravariantFunctor[OWrites] = new ContravariantFunctor[OWrites] {
Expand Down
2 changes: 2 additions & 0 deletions project/plugins.sbt
Expand Up @@ -7,6 +7,8 @@ resolvers += Resolver.typesafeRepo("releases")

addSbtPlugin("com.typesafe.play" % "interplay" % sys.props.get("interplay.version").getOrElse("1.3.5"))

addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.24")

addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.1.13")

addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "1.1")
Expand Down

0 comments on commit 651b57e

Please sign in to comment.