Speeding up RESTful Services in Play Framework

Speeding up RESTful Services in Play Framework thumbnail

Here at Lucid Software we use a hypermedia-driven application architecture. This means that a client that uses our micro-services simply has to hard-code a “bootstrap” resource URI, and we can re-jigger our endpoints whenever we choose. This approach gives us flexibility on the back-end while maintaining perfect compatibility with our web app, iOS app, and other clients. The following is an example response for a hypermedia resource describing a document in our system:

{
  "attributes": "https://documents.lucidchart.com/documents/ff0069a7-bf80-4f29-bdf0-aeb6cc8da593/attributes",
  "created": "2015-07-16T04:46:35Z",
  "creator": "https://users.lucidchart.com/users/102485935",
  "documentUsers": "https://documents.lucidchart.com/documents/ff0069a7-bf80-4f29-bdf0-aeb6cc8da593/users",
  "edit": "https://www.lucidchart.com/documents/edit/ff0069a7-bf80-4f29-bdf0-aeb6cc8da593",
  "id": "ff0069a7-bf80-4f29-bdf0-aeb6cc8da593",
  "invitations": "https://documents.lucidchart.com/documents/ff0069a7-bf80-4f29-bdf0-aeb6cc8da593/invitations",
  "pages": "https://documents.lucidchart.com/documents/ff0069a7-bf80-4f29-bdf0-aeb6cc8da593/pages",
  "saved": "2016-08-17T16:33:58Z",
  "size": 241278,
  "template": "https://documents.lucidchart.com/documents/ff0069a7-bf80-4f29-bdf0-aeb6cc8da593/template",
  "thumb": "https://www.lucidchart.com/documents/thumb/ff0069a7-bf80-4f29-bdf0-aeb6cc8da593/0/241278/NULL/400",
  "title": "Basic Use Case Diagram",
  "uri": "https://documents.lucidchart.com/documents/ff0069a7-bf80-4f29-bdf0-aeb6cc8da593",
  "usersWithAccess": "https://documents.lucidchart.com/documents/ff0069a7-bf80-4f29-bdf0-aeb6cc8da593/users/access"
}

Most of these links are never used all at once. A typical client would receive a response and only follow the links they’re looking for.

Our mission at Lucid Software is to make work better, and we have a high standard for our application performance. We recently eliminated two common inefficiencies from our RESTful services.

1. java.net.URI is slow. We can get faster type safety.

Below is an example of a RESTful link class containing a URI as its only member. A typical REST resource would have many similar members.

case class FooLink(uri: java.net.URI)

java.net.URI’s constructor uses a parser to check the validity of the string used to instantiate it. The URI parser is useful for validating hyperlinks to catch errors before attempting HTTP requests. However, given enough URIs, instantiation time can add up. We noticed this when we were investigating timing spikes on one of our REST endpoints. Twenty links times thousands of users was costing us a significant amount of server power to compute:

Consequently, we chose to do away with the URI parser and other overhead of java.net.URI. We replaced it with a new class, a FastURI that basically wraps a String. We control the generation of the links on the back-end, so we can guarantee that they conform to the pattern for which the java.net.URI parser was testing.

case class FastURI(uri: String)
case class FooLink(uri: FastURI)

On the rare occasion where we need to validate a hyperlink, we make a new URI(FastURI.uri). Type safety is preserved with the FastURI type, and we still get the benefits of the URI parser in java.net.URI, but only when we need it. We use Scala, but this principle will apply to any JVM language.

2. Play’s functional syntax for JSON combinators may be elegant, but it’s also costly.

Another choke point for our endpoints was our JSON serialization. We use a fork of Play 2.3.8, and we discovered some unnecessary work being done by Play’s api.libs.functional.syntax._ when used to generate a JSON serializer. A common Play JSON formatter looks like this:

case class Foo (
    do: FooLink,
    re: FooLink,
    mi: FooLink,
    fa: FooLink
)
object Foo {
    implicit val format = Format[Foo] (
        (JsPath / “do”) and
        (JsPath / “re”) and
        (JsPath / “mi”) and
        (JsPath / “fa”) )
        (apply, unlift(unapply))
}

The “and” operators dynamically nest functions inside each other every time the formatter is used. Doing so is powerful in that it allows Play to serialize complex, recursive structures; however, it costs extra time and is not necessary for REST objects made up of links and simple object data. It is more efficient to specify the JSON marshaller and the JSON parser separately without the functional syntax, as shown below:

implicit val reads: Reads[Foo] = Reads[Foo] { json =>
    for {
        do <- (json \ "do").validate[FooLink]
        re <- (json \ "re").validate[FooLink]
        mi <- (json \ "mi").validate[FooLink]
        fa <- (json \ "fa").validate[FooLink]
    } yield {
        Foo(do, re, mi, fa)
    }
}

implicit val writes: Writes[Foo] = Writes[Foo] { foo =>
    Json.obj(
        "do" -> foo.do,
        "re" -> foo.re,
        "mi" -> foo.mi,
        "fa" -> foo.fa
    )
}

implicit val format = Format(reads,writes)

This code avoids the layered functional syntax and performs much faster. It is twice as long, so we created a scala macro to generate the boilerplate code in one line. Play also provides a macro for creating JSON combinators, but it is important to note that the Play macro generates code with the slower functional syntax.

Result: a 1.5x improvement.

As a result of our tackling these two inefficiencies, we saw around a 50% decrease in the time it takes to create and serialize our REST objects to JSON. This figure is the result of a benchmark of a REST object with ten links tested before and after these optimizations. In addition, we saw great improvement in the endpoint that originally caught our attention. This graph shows the weeks before and after we released the change:

As we grow and serve an ever-increasing user base, it is important to consider our scalability. We are always searching for slow code that we can refactor before it wakes up our Ops team in the middle of the night due to a service that is overburdened and timing out. And most importantly, we want our services to be completely dependable as more and more people share their ideas visually with Lucidchart and Lucidpress.

6 Comments

  1. Thanks for this blog post, it’s great to see hard data about Play’s performance! Do you know what proportion of the speedup was due to not using the play-json functional combinators? We’ve always known they’re slow, but there’s a big difference between showing how slow they are in a microbenchmark and seeing how much their performance impacts a real world application.

  2. Matthew BarlockerAugust 30, 2016 at 8:55 am

    Nailed it.

  3. Joshua ReynoldsAugust 30, 2016 at 11:38 am

    @James Roper: I am glad others have recognized the same issue. About half of our improvement came from avoiding the functional combinators – according to local tests. But it is hard to be exact because we released both fixes at the same time.

  4. Steve LustbaderJanuary 19, 2017 at 9:55 am

    Is the scala macro you wrote to generate the faster JSON boilerplate code available as open source or contributed back to the Play framework?

  5. Enver OsmanovJanuary 20, 2017 at 2:21 am

    What tool do you use for benchmarking and monitoring?

  6. @Steve Lustbader: it has not been contributed to play and is not currently an open source project. I’m not opposed to eventually publishing it but it needs some cleanup work and fails in certain cases where Play’s macro succeeds. We have a fairly narrow use case and generally have fairly simple json structure. That’s also why it hasn’t been contributed back to play. The macro in play is more about eliminating boiler plate than worrying about serialization performance.

    play-json has also been split into its own project making it break from play’s release cycle and (hopefully) paving the way for improvements like what Lucid has done with the macro.

Your email address will not be published.