diff --git a/benchmarks/src/main/scala/play/api/libs/json/HashCodeCollider.scala b/benchmarks/src/main/scala/play/api/libs/json/HashCodeCollider.scala new file mode 100644 index 000000000..fb53f5dc8 --- /dev/null +++ b/benchmarks/src/main/scala/play/api/libs/json/HashCodeCollider.scala @@ -0,0 +1,91 @@ +/** + * Original code at: https://github.com/plokhotnyuk/jsoniter-scala/blob/bb4837d/jsoniter-scala-benchmark/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/macros/HashCodeCollider.scala + * + * MIT License + * + * Copyright (c) 2017 Andriy Plokhotnyuk, and respective contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package play.api.libs.json + +import scala.collection.mutable.ArrayBuffer + +object HashCodeCollider { + val zeroHashCodeStrings: collection.Seq[String] = { + val cs = new ArrayBuffer[String](2 * 1024 * 1024) + var i0 = 33 + while (i0 < 127) { + val h0 = i0 * 31 + if (i0 != '\\' && i0 != '"') { + var i1 = 33 + while (i1 < 127) { + val h1 = (h0 + i1) * 31 + if (i1 != '\\' && i1 != '"') { + var i2 = 33 + while (i2 < 127) { + val h2 = (h1 + i2) * 31 + if ((((h2 + 32) * 923521) ^ ((h2 + 127) * 923521)) < 0 && i2 != '\\' && i2 != '"') { + var i3 = 33 + while (i3 < 127) { + val h3 = (h2 + i3) * 31 + if ((((h3 + 32) * 29791) ^ ((h3 + 127) * 29791)) < 0 && i3 != '\\' && i3 != '"') { + var i4 = 33 + while (i4 < 127) { + val h4 = (h3 + i4) * 31 + if ((((h4 + 32) * 961) ^ ((h4 + 127) * 961)) < 0 && i4 != '\\' && i4 != '"') { + var i5 = 33 + while (i5 < 127) { + val h5 = (h4 + i5) * 31 + if ((((h5 + 32) * 31) ^ ((h5 + 127) * 31)) < 0 && i5 != '\\' && i5 != '"') { + var i6 = 33 + while (i6 < 127) { + val h6 = (h5 + i6) * 31 + if (((h6 + 32) ^ (h6 + 127)) < 0 && i6 != '\\' && i6 != '"') { + var i7 = 33 + while (i7 < 127) { + if (h6 + i7 == 0 && i7 != '\\' && i7 != '"') { + cs += s"${i0.toChar}${i1.toChar}${i2.toChar}${i3.toChar}${i4.toChar}${i5.toChar}${i6.toChar}${i7.toChar}" + } + i7 += 1 + } + } + i6 += 1 + } + } + i5 += 1 + } + } + i4 += 1 + } + } + i3 += 1 + } + } + i2 += 1 + } + } + i1 += 1 + } + } + i0 += 1 + } + cs + } +} diff --git a/benchmarks/src/main/scala/play/api/libs/json/JsonParsing_01_ParseManyFields.scala b/benchmarks/src/main/scala/play/api/libs/json/JsonParsing_01_ParseManyFields.scala new file mode 100644 index 000000000..ce000b91e --- /dev/null +++ b/benchmarks/src/main/scala/play/api/libs/json/JsonParsing_01_ParseManyFields.scala @@ -0,0 +1,23 @@ +package play.api.libs.json + +import org.openjdk.jmh.annotations._ + +@State(Scope.Benchmark) +class JsonParsing_01_ParseManyFields { + @Param(Array("10", "100", "1000", "10000", "100000")) + var n: Int = 100 + + var stringToParse: String = _ + + @Setup + def setup(): Unit = { + val value = "42" + stringToParse = HashCodeCollider.zeroHashCodeStrings.take(n) + .mkString("""{"s":"s","""", s"""":$value,"""", s"""":$value,"i":1}""") + } + + @Benchmark + def parseObject(): Unit = { + Json.parse(stringToParse) + } +} diff --git a/play-json/shared/src/main/scala/play/api/libs/json/JsPath.scala b/play-json/shared/src/main/scala/play/api/libs/json/JsPath.scala index 7700af56e..6f7577e5e 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/JsPath.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/JsPath.scala @@ -159,7 +159,7 @@ object JsPath extends JsPath(List.empty) { } // optimize fast path - val objectMap = new scala.collection.mutable.LinkedHashMap[String, JsValue]() + val objectMap = JsObject.createFieldsMap() val isSimpleObject = pathValues.forall { case (JsPath(KeyPathNode(key) :: Nil), value) => objectMap.put(key, value) diff --git a/play-json/shared/src/main/scala/play/api/libs/json/JsValue.scala b/play-json/shared/src/main/scala/play/api/libs/json/JsValue.scala index 3103c97f1..38cc29b05 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/JsValue.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/JsValue.scala @@ -200,10 +200,21 @@ case class JsObject( } object JsObject extends (Seq[(String, JsValue)] => JsObject) { + + /** + * INTERNAL API: create a fields map by wrapping a Java LinkedHashMap. + * + * We use this because the Java implementation better handles hash code collisions for Comparable keys. + */ + private[json] def createFieldsMap(fields: Iterable[(String, JsValue)] = Seq.empty): mutable.Map[String, JsValue] = { + import scala.collection.JavaConverters._ + new java.util.LinkedHashMap[String, JsValue]().asScala ++= fields + } + /** * Construct a new JsObject, with the order of fields in the Seq. */ - def apply(fields: collection.Seq[(String, JsValue)]): JsObject = new JsObject(mutable.LinkedHashMap(fields.toSeq: _*)) + def apply(fields: collection.Seq[(String, JsValue)]): JsObject = new JsObject(createFieldsMap(fields)) def empty = JsObject(Seq.empty) } diff --git a/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala b/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala index f86f65f11..6175b50c7 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala @@ -101,7 +101,7 @@ object OWrites extends PathWrites with ConstraintWrites { def writeFields(fieldsMap: mutable.Map[String, JsValue], a: A): Unit def writes(a: A): JsObject = { - val fieldsMap = new mutable.LinkedHashMap[String, JsValue]() + val fieldsMap = JsObject.createFieldsMap() writeFields(fieldsMap, a) JsObject(fieldsMap) }