argonaut-io/argonaut

Writing already encoded JSON to string is very slow

mrdziuban opened this issue · 3 comments

I've found that writing large chunks of already encoded JSON to string using argonaut is incredibly slow compared to circe -- up to 940x slower. I've put together this simple example to compare the two: https://github.com/mrdziuban/json-writing-test.

That example generates and writes three JSON objects, one that has a plain text string as the value, one that has an array of strings as the value, and one that has a JSON encoded array of strings as the value. Writing the last object to a string takes over 14 seconds with argonaut but only ~30 milliseconds with circe. Here's the output from running the example:

Generating UUIDs took 274ms
Generating argonaut JSON took 131ms
Generating circe JSON took 127ms

**********************************************************************
Argonaut
**********************************************************************
Writing plain text argonaut JSON with nospaces took 6ms
Writing plain text argonaut JSON with spaces2 took 6ms

Writing JSON array argonaut JSON with nospaces took 17ms
Writing JSON array argonaut JSON with spaces2 took 14ms

Writing JSON text argonaut JSON with nospaces took 14316ms
Writing JSON text argonaut JSON with spaces2 took 14122ms


**********************************************************************
Circe
**********************************************************************
Writing plain text circe JSON with nospaces took 3ms
Writing plain text circe JSON with spaces2 took 3ms

Writing JSON array circe JSON with nospaces took 11ms
Writing JSON array circe JSON with spaces2 took 9ms

Writing JSON text circe JSON with nospaces took 33ms
Writing JSON text circe JSON with spaces2 took 15ms

Is there some logic that is handling encoded JSON strings specially? Thanks in advance!

A little more investigation leads me to believe the performance issues are with the string escaping. Here's an even simpler example using the list of special characters:

import argonaut.{Json => AJson}
import io.circe.{Json => CJson}
import java.time.Instant

def timed[A](action: String, fn: () => A): A = {
  val start = Instant.now
  val res = fn()
  println(s"$action took ${Instant.now.toEpochMilli - start.toEpochMilli}ms")
  res
}

timed("argonaut", () => AJson.jString(AJson.array(
  Seq.tabulate(25000)(_ => AJson.jString("\\ \" \b \f \n \r \t")):_*).nospaces).nospaces)
// argonaut took 25021ms

timed("circe", () => CJson.fromString(CJson.arr(
  Seq.tabulate(25000)(_ => CJson.fromString("\\ \" \b \f \n \r \t")):_*).noSpaces).noSpaces)
// circe took 35ms

I just saw #300, which seems to address the same issue. Running the test above with that branch brings argonaut's time down to ~50ms. That's probably good enough, but I'll leave this open for now in case it's still worth investigating the slight difference in performance.

@mrdziuban I've just pushed Argonaut 6.2.2.