Composable and streamable Play apps
Upcoming SlideShare
Loading in...5
×
 

Composable and streamable Play apps

on

  • 22,574 views

Web pages can get very complex and slow. In this talk, I share how we solve some of these problems at LinkedIn by leveraging composition and streaming in the Play Framework. This was my keynote for ...

Web pages can get very complex and slow. In this talk, I share how we solve some of these problems at LinkedIn by leveraging composition and streaming in the Play Framework. This was my keynote for Ping Conference 2014 ( http://www.ping-conf.com/ ): the video is on ustream ( http://www.ustream.tv/recorded/42801129 ) and the sample code is on github ( https://github.com/brikis98/ping-play ).

Statistics

Views

Total Views
22,574
Views on SlideShare
17,023
Embed Views
5,551

Actions

Likes
135
Downloads
416
Comments
8

39 Embeds 5,551

http://engineering.linkedin.com 4079
https://engineering.linkedin.com 473
http://www.scoop.it 370
http://feedly.com 310
http://www.newsblur.com 61
https://twitter.com 55
http://t.co 52
http://newsblur.com 23
http://www.mybestcv2.co.il 21
http://www.feedspot.com 21
http://digg.com 20
http://getpocket.com 14
http://www.linkedin.com 6
http://linkedin516.rssing.com 4
https://www.google.com 4
http://reader.aol.com 4
http://webmakers.io 3
https://www.inoreader.com 3
http://127.0.0.1 3
https://newsvi.be 2
https://www.linkedin.com 2
http://www.google.ca 2
https://reader.aol.com 2
http://feedreader.com 2
http://desecrater27.rssing.com 1
http://gladyspintado.wordpress.com 1
http://news.google.com 1
https://home.jolicloud.com 1
http://wave.webaim.org 1
https://inoreader.com 1
http://honyaku.yahoofs.jp 1
http://webcache.googleusercontent.com 1
https://feedly.com 1
https://schalanda.name 1
http://www.inoreader.com 1
http://www.google.com 1
http://xianguo.com 1
https://www.facebook.com 1
http://blog.knoldus.com 1
More...

Accessibility

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel

18 of 8 Post a comment

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
  • Very informative. Awesome!
    Are you sure you want to
    Your message goes here
    Processing…
  • Eu estou presizado de investidore, para investimento em uma mineradora de ferro já funsionado, com alto retorno no investimento. mais informações email: vni.arlindo@hotmail.com
    Are you sure you want to
    Your message goes here
    Processing…
  • excellent broooo
    Are you sure you want to
    Your message goes here
    Processing…
  • Excellent enjoyed.
    Thanks for sharing it mate.
    Are you sure you want to
    Your message goes here
    Processing…
  • Great slideshow. LinkedIn keeps innovating. Thanks for sharing how you use Play/Scala at LinkedIn.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

    Composable and streamable Play apps Composable and streamable Play apps Presentation Transcript

    • at Composable and streamable Play apps
    • About me Part of the Service Infrastructure team, which is bringing the Play Framework to LinkedIn engineers.
    • At LinkedIn, we’ve been running Play apps in production for over a year
    • More than 60 apps on Play now, including:
    • Channels
    • Recruiter Mobile
    • Jobs
    • Polls in Groups
    • Play Labs (e.g. InDemand)
    • Many internal tools (e.g. REST search)
    • Plus many backends services that don’t have sexy screenshots
    • We love Play… but we’ve had a couple tiny problems:
    • We love Play… but we’ve had a couple tiny problems: 1. Unmanageable complexity
    • We love Play… but we’ve had a couple tiny problems: 1. Unmanageable complexity 2. Terrible performance
    • It’s probably not what you think.
    • This talk is about a couple techniques to deal with complexity and performance problems.
    • These techniques are experimental.
    • But fun.
    • Enjoy!
    • You can find the code from this presentation at: https://github.com/brikis98/ping-play
    • Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
    • Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
    • Web pages can get complex
    • For example, consider the LinkedIn home page
    • Almost every part of the page is dynamic, highly customized for the user, and interactive.
    • Trying to cram this all into one controller and one template is completely unmaintainable
    • “Managing complexity is the most important technical topic in software development.” Steve McConnell, Code Complete
    • Abstraction and Composition
    • “Abstraction is the ability to engage with a concept while safely ignoring some of its details.” Steve McConnell, Code Complete
    • Abstraction allows you to take a simpler view of a complex concept
    • Composition: assemble complex behavior by combining simpler behavior
    • Abstraction: structure your app so you can focus on one part while safely ignoring the rest
    • Abstraction: structure your app so you can focus on one part while safely ignoring the rest Composition: structure your app so you can easily combine the simpler parts into more complicated parts
    • Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
    • We’ll start by building a completely standalone “Who’s Viewed Your Profile” (WVYP) module
    • We need: 1. Data: WVYP count, search count 2. Template: to render the HTML 3. Controller: to tie it all together
    • Backend Server Frontend Server Internet Load Balancer Backend Server Frontend Server Data Store Backend Server Frontend Server Backend Server Backend Server Data Store Data Store Data Store For data, LinkedIn uses a Service Oriented Architecture
    • For this talk, we’re going to simulate service calls by calling a mock endpoint
    • object Mock extends Controller { def mock(serviceName: String) = Action.async { serviceName match { case "wvyp" => respond(data = "56", delay = 10) case "search" => respond(data = "10", delay = 5) case "likes" => respond(data = "150", delay = 40) case "comments" => respond(data = "14", delay = 20) } } private def respond(data: String, delay: Long) = Promise.timeout(Ok(data), delay) } app/mock/Mock.scala For each “service”, this endpoint returns mock data after a fixed delay. In the real world, the data might be JSON.
    • GET /mock/:serviceName controllers.Mock.mock(serviceName: String) conf/routes The routes entry for the mock endpoint
    • object ServiceClient { def makeServiceCall(serviceName: String): Future[String] = { WS.url(s"http://localhost:9000/mock/$serviceName").get().map(_.body) } } app/data/ServiceClient.scala A simple client to make a remote call to the mock endpoint and return a Future with the body of the HTTP response
    • @(wvypCount: Int, searchCount: Int) <html> <head> <link rel="stylesheet" href="/assets/stylesheets/wvyp.css"/> </head> <body> <div class="wvyp"> <h2>Who's Viewed Your Profile</h2> @views.html.wvyp.wvypCount(wvypCount) @views.html.wvyp.searchCount(searchCount) </div> </body> </html> app/views/wvyp/wvyp.scala.html For the template, we use Play’s Scala templates (.scala. html). This template uses two partials for the body.
    • @(wvypCount: Int) <p class="wvyp-count"> <span class="large-number">@wvypCount</span> <span>Your profile has been viewed by <b>@wvypCount</b> people in the past 3 days</span> </p> app/views/wvyp/wvypCount.scala.html @(searchCount: Int) <p class="search-count"> <span class="large-number">@searchCount</span> <span>Your have shown up in search results <b>@searchCount</b> times in the past 3 days</span> </p> app/views/wvyp/searchCount.scala.html The markup for the two partials that show the counts
    • object WVYP extends Controller { def index = Action.async { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") } } app/controllers/WVYP.scala Next, the WVYP controller. First, we make two service calls in parallel to fetch the WVYP count and search count.
    • object WVYP extends Controller { def index = Action.async { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") for { wvypCount <- wvypCountFuture searchCount <- searchCountFuture } yield { Ok(views.html.wvyp.wvyp(wvypCount.toInt, searchCount.toInt)) } } } app/controllers/WVYP.scala Next, we compose the two Futures into a single Future and render the WVYP template.
    • GET /mock/:serviceName controllers.Mock.mock(serviceName: String) GET /wvyp controllers.WVYP.index conf/routes Add a routes entry for the WVYP controller
    • The result
    • We now have one small module
    • Now, imagine we also have another standalone module called “Who’s Viewed Your Updates” (WVYU)
    • GET /mock/:serviceName controllers.Mock.mock(serviceName: String) GET /wvyp controllers.WVYP.index GET /wvyu controllers.WVYU.index conf/routes It has a controller, template, and routes entry just like WVYP
    • How can we combine the two modules?
    • trait Action[A] extends EssentialAction { def apply(request: Request[A]): Future[SimpleResult] } play/api/mvc/Action.scala In Play, an Action is a function
    • Request[A] => Future[SimpleResult] The actions in each standalone module are just functions from Request to Result. Functions can be composed!
    • @(wvypCount: Int, searchCount: Int) <html> <head> <link rel="stylesheet" href="/assets/stylesheets/wvyp.css"/> </head> <body> @views.html.wvyp.wvypBody(wvypCount, searchCount) </body> </html> app/views/wvyp/wvyp.scala.html We need one change to each module: move the body into a partial. Scala templates are functions, so they also compose!
    • def index(embed: Boolean) = Action.async { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") for { wvypCount <- wvypCountFuture searchCount <- searchCountFuture } yield { if (embed) Ok(views.html.wvyp.wvypBody(wvypCount.toInt, searchCount.toInt)) else Ok(views.html.wvyp.wvyp(wvypCount.toInt, searchCount.toInt)) } } app/controllers/WVYP.scala Update each module’s controller to accept an “embed” parameter: if it’s set to true, render only the body partial.
    • GET /mock/:serviceName controllers.Mock.mock(serviceName: String) GET /wvyp controllers.WVYP.index(embed: Boolean ?= false) GET /wvyu controllers.WVYU.index(embed: Boolean ?= false) conf/routes Update the routes file too
    • object Pagelet { def readBody(result: SimpleResult)(implicit codec: Codec): Future[Html] = { result.body.run(Iteratee.consume()).map(bytes => Html(new String(bytes, codec.charset))) } } app/ui/Pagelet.scala Add a helper that can take a SimpleResult and return its body as Future[Html]. We’ll talk more about Iteratees later.
    • object Aggregator extends Controller { def index = Action.async { request => val wvypFuture = Wvyp.index(embed = true)(request) val wvyuFuture = Wvyu.index(embed = true)(request) } } app/controllers/Aggregator.scala Now for the aggregator controller. First, we call the two submodules. Each returns a Future[SimpleResult].
    • object Aggregator extends Controller { def index = Action.async { request => val wvypFuture = Wvyp.index(embed = true)(request) val wvyuFuture = Wvyu.index(embed = true)(request) for { wvyp <- wvypFuture wvyu <- wvyuFuture wvypBody <- Pagelet.readBody(wvyp) wvyuBody <- Pagelet.readBody(wvyu) } yield { } } } app/controllers/Aggregator.scala Read the body of each Future[SimpleResult] as Html using the Pagelet helper we just added
    • object Aggregator extends Controller { def index = Action.async { request => val wvypFuture = Wvyp.index(embed = true)(request) val wvyuFuture = Wvyu.index(embed = true)(request) for { wvyp <- wvypFuture wvyu <- wvyuFuture wvypBody <- Pagelet.readBody(wvyp) wvyuBody <- Pagelet.readBody(wvyu) } yield { Ok(views.html.aggregator.aggregator(wvypBody, wvyuBody)) } } } app/controllers/Aggregator.scala Pass the Html to the aggregator template
    • GET @(wvypBody:/mock/:serviceName controllers.Mock.mock(serviceName: String) Html, wvyuBody: Html) GET /wvyp controllers.WVYP.index(embed: Boolean ?= false) GET <html> /wvyu controllers.WVYU.index(embed: Boolean ?= false) GET <head> /aggregate controllers.Aggregator.index <link rel="stylesheet" href="/assets/stylesheets/wvyp.css"/> <link rel="stylesheet" href="/assets/stylesheets/wvyu.css"/> </head> <body> @wvypBody @wvyuBody </body> </html> app/views/aggregator/aggregator.scala Add the aggregator to the routes file
    • The result. We’ve composed two Play modules!
    • Wins 1. Abstraction: we can focus on just one small part of the page while safely ignoring all the rest. 2. Composition: we can build complicated pages by putting together simpler parts. We can get lots of reuse from common pieces. 3. Testing and iterating on a small, standalone unit is much easier and faster.
    • Caveats 1. Standalone modules may be inefficient in terms of duplicated service calls. However, de-duping is straightforward. 2. Have to merge and dedupe static content.
    • Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
    • The composition code glosses over a few details
    • Some questions: 1. How do we set cookies? 2. How do we handle errors? 3. How do we aggregate static content?
    • It turns out HTTP is an elegant way to answer these questions.
    • object WVYP extends Controller { def index(embed: Boolean) = Action { // [snip] Ok(views.html.wvyp.wvyp(wvypCount.toInt, searchCount.toInt)).withCookies(Cookie(“foo”, “bar”)) } } app/controllers/WVYP.scala For example, imagine a standalone module sets a Cookie
    • object Aggregator extends Controller { def index = Action.async { request => val wvypFuture = Wvyp.index(embed = true)(request) for { wvyp <- wvypFuture wvypBody <- Pagelet.readBody(wvyp) wvypHeaders = wvyp.header.headers // The Cookie header here will contain the “foo” cookie! } yield { Ok(views.html.aggregator.aggregator(wvypBody)) } } } app/controllers/Aggregator.scala The aggregator will see that as a Cookie header!
    • object Pagelet { def mergeCookies(results: SimpleResult*): Seq[Cookie] = { results.flatMap { result => result.header.headers.get(HeaderNames.SET_COOKIE).map(Cookies.decode).getOrElse(Seq.empty) } } } app/ui/Pagelet.scala We can add a helper to merge the cookies from multiple SimpleResult objects
    • def index = Action.async { request => val wvypFuture = Wvyp.index(embed = true)(request) val wvyuFuture = Wvyu.index(embed = true)(request) for { wvyp <- wvypFuture wvyu <- wvyuFuture wvypBody <- Pagelet.readBody(wvyp) wvyuBody <- Pagelet.readBody(wvyu) } yield { Ok(views.html.aggregator.aggregator(wvypBody, wvyuBody)) .withCookies(Pagelet.mergeCookies(wvyp, wvyu):_*) } } app/controllers/Aggregator.scala Update the aggregator to write out the merged cookies
    • We can see the aggregate endpoint is now setting cookies
    • HTTP headers are a good way to handle error cases too.
    • A quick review of HTTP status codes
    • For example, if a module has no content, return a 204
    • Invalid module request? Return 404.
    • Plus other important status codes
    • def index = Action.async { request => val wvypFuture = Wvyp.index(embed = true)(request) for { wvyp <- wvypFuture wvypBody <- Pagelet.readBody(wvyp) } yield { if (wvyp.status == 200) { Ok(views.html.aggregator.aggregator(wvypBody)) } else { // Handle errors } } } app/controllers/Aggregator.scala The aggregator can read the status codes and react accordingly
    • CSS and JS dependencies can be set as a header and aggregated too.
    • object StaticContent { val cssHeaderName = "X-CSS" val jsHeaderName = "X-JS" def asHeaders(css: Seq[String], js: Seq[String]): Seq[(String, String)] = { Seq(cssHeaderName -> css.mkString(","), jsHeaderName -> js.mkString(",")) } } app/ui/StaticContent.scala Add a helper to create the headers
    • def index(embed: Boolean) = Action { // [...] all other code is the same as before [...] val css = Vector("/assets/stylesheets/wvyp.css") val js = Vector.empty[String] if (embed) { Ok(views.html.wvyp.wvypBody(wvypCount.toInt, searchCount.toInt)) .withHeaders(StaticContent.asHeaders(css, js):_*) } else { Ok(views.html.wvyp.wvyp(wvypCount.toInt, searchCount.toInt, css, js)) } } app/controllers/WVYP.scala In embed mode, return CSS/JS dependencies as headers. In standalone mode, render them in the template.
    • object StaticContent { def mergeCssHeaders(results: SimpleResult*): Seq[String] = mergeHeaderValues(cssHeaderName, results: _*) def mergeJsHeaders(results: SimpleResult*): Seq[String] = mergeHeaderValues(jsHeaderName, results:_*) private def mergeHeaderValues(headerName: String, results: SimpleResult*): Seq[String] = { results.flatMap { result => result.header.headers.get(headerName).map(_.split(",").toSeq).getOrElse(Seq.empty) }.distinct } } app/ui/StaticContent.scala Helpers to merge CSS and JS headers from multiple SimpleResult objects
    • def index = Action.async { request => val wvypFuture = Wvyp.index(embed = true)(request) val wvyuFuture = Wvyu.index(embed = true)(request) for { wvyp <- wvypFuture wvyu <- wvyuFuture wvypBody <- Pagelet.readBody(wvyp) wvyuBody <- Pagelet.readBody(wvyu) } yield { val css = StaticContent.mergeCssHeaders(wvyp, wvyu) val js = StaticContent.mergeCssHeaders(wvyp, wvyu) Ok(views.html.aggregator.aggregator(wvypBody, wvyuBody, css, js)) } } app/controllers/Aggregator.scala The aggregator can merge and de-dupe the static content and pass it to its template for rendering
    • Wins 1. Modules are truly standalone 2. Can dynamically compose modules using Play’s router 3. Can compose modules from remote endpoints 4. Can reuse endpoints from the browser via AJAX 5. Static content dependencies explicitly defined
    • Caveats 1. Managing static content in a controller, instead of a view, feels clunky.
    • Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
    • So far, we’ve been using a mock endpoint to simulate remote service calls
    • What happens if one of the remote calls is slow?
    • object Mock extends Controller { def mock(serviceName: String) = Action.async { serviceName match { case "wvyp" => respond(data = "56", delay = 10) case "search" => respond(data = "10", delay = 2000) // SLOW! case "likes" => respond(data = "150", delay = 40) case "comments" => respond(data = "14", delay = 20) } } private def respond(data: String, delay: Long) = Promise.timeout(Ok(data), delay) } app/mock/Mock.scala As an extreme example, let’s make the search service take two seconds to respond
    • Time to first byte is 2 seconds! Everything has to wait for the one slow service, including static content.
    • Is there a way to start sending the response before all the data is available?
    • Facebook solves this problem with BigPipe https://www.facebook.com/note.php?note_id=389414033919
    • Can we build BigPipe with Play?
    • (Well, yea, or I wouldn’t have made this talk)
    • We can use Enumerators!
    • Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
    • Enumerators are part of the Play Iteratees library.
    • Quick Review 1. An Enumerator is a Producer. It pumps out chunks of data. 2. An Iteratee is a Consumer. It reactively consumes chunks of data. 3. An Enumeratee is an Adapter. You can attach them in front of Iteratees and Enumerators to filter the chunks of data.
    • Producer, consumer, and adapter
    • These are all composable abstractions for working with streams of data
    • Let’s look at some examples
    • object EnumeratorExample extends Controller { def index = Action { Ok.chunked(/* We need an Enumerator here */) } } app/controllers/EnumeratorExamples.scala Play has an Ok.chunked method which can stream out the contents of an Enumerator
    • object EnumeratorExample extends Controller { def index = Action { Ok.chunked(Enumerator("Created", " using", " Enumerator", ".apply()nn")) } } app/controllers/EnumeratorExamples.scala The Enumerator object has several factory methods. For example, Enumerator.apply creates one from a fixed list.
    • Basic “Hello World” example using Enumerator.apply.
    • object EnumeratorExample extends Controller { def index = Action { Ok.chunked(Enumerator.repeatM(Promise.timeout("Hellon", 500))) } } app/controllers/EnumeratorExamples.scala You can also create an Enumerator that repeats a value generated from a Future
    • The word “Hello” is pumped out every 500ms. Note: when testing streaming, use “curl -N” so curl doesn’t buffer.
    • def index = Action { val helloEnumerator = Enumerator("hello ") val goodbyeEnumerator = Enumerator("goodbyenn") val helloGoodbyeEnumerator = helloEnumerator.andThen(goodbyeEnumerator) Ok.chunked(helloGoodbyeEnumerator) } app/controllers/EnumeratorExamples.scala Most importantly, you can combine Enumerators. Here is an example using the andThen method.
    • With andThen, we see all the data from the first enumerator and then all the data from the second one
    • def index = Action { val helloEnumerator = Enumerator.repeatM(Promise.timeout("Hellon", 500)) val goodbyeEnumerator = Enumerator.repeatM(Promise.timeout("Goodbyen", 1000)) val helloGoodbyeEnumerator = Enumerator.interleave(helloEnumerator, goodbyeEnumerator) Ok.chunked(helloGoodbyeEnumerator) } app/controllers/EnumeratorExamples.scala We can also combine Enumerators using Enumerator. interleave
    • With interleave, data can come in any order. Above, we see “Hello” every 500ms and “Goodbye” every 1000ms.
    • Let’s use Enumerators to stream the results of our remote service calls
    • object WVYPEnumerator extends Controller { def index = Action { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") } } app/controllers/WVYPEnumerator.scala Create the new WVYP controller. Again, we start with two service calls.
    • object WVYPEnumerator extends Controller { def index = Action { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") val wvypCountEnum = Enumerator.flatten(wvypCountFuture.map(str => Enumerator(str + "n"))) val searchCountEnum = Enumerator.flatten(searchCountFuture.map(str => Enumerator(str + "n"))) } } app/controllers/WVYPEnumerator.scala Next, convert each Future[String] into an Enumerator[String]
    • object WVYPEnumerator extends Controller { def index = Action { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") val wvypCountEnum = Enumerator.flatten(wvypCountFuture.map(str => Enumerator(str + "n"))) val searchCountEnum = Enumerator.flatten(searchCountFuture.map(str => Enumerator(str + "n"))) val body = wvypCountEnum.andThen(searchCountEnum) Ok.chunked(body) } } app/controllers/WVYPEnumerator.scala Finally, compose the two Enumerators and use Ok.chunked to stream them out
    • GET @(wvypBody:/mock/:serviceName controllers.Mock.mock(serviceName: String) Html, wvyuBody: Html) GET /wvyp controllers.WVYP.index(embed: Boolean ?= false) GET <html> /wvyu controllers.WVYU.index(embed: Boolean ?= false) GET <head> /aggregate controllers.Aggregator.index GET <link rel="stylesheet" href="/assets/stylesheets/wvyp.css"/> /wvyp/enumerator controllers.WVYPEnumerator.index <link rel="stylesheet" href="/assets/stylesheets/wvyu.css"/> </head> <body> @wvypBody @wvyuBody </body> </html> app/views/aggregator/aggregator.scala Add a routes entry for the enumerator-based WVYP controller
    • Almost immediately, we see “56”, from the wvyp service.
    • 2 seconds later, we see “10”, from the search service.
    • We’re streaming!
    • However, we’re only streaming plain text. How do we stream a template?
    • Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
    • play.Keys.templatesTypes ++= Map("stream" -> "ui.HtmlStreamFormat") play.Keys.templatesImport ++= Vector("ui.HtmlStream", "ui.HtmlStream._", "ui.StaticContent") build.sbt Play allows you to define a new template type. This will allow us to create .scala.stream files instead of .scala.html.
    • The new template type must define an “Appendable” and a “Format” for it
    • trait Appendable[T] { def +=(other: T): T } play/api/templates/Templates.scala The Appendable trait is simple: it’s anything with an append (+=) operator.
    • class Html(buffer: StringBuilder) extends Appendable[Html](buffer) { def +=(other: Html) = { buffer.append(other.buffer) this } } play/api/templates/Templates.scala The default Appendable built into Play is Html, which just wraps a StringBuilder.
    • case class HtmlStream(enumerator: Enumerator[Html]) extends Appendable[HtmlStream] { def +=(other: HtmlStream): HtmlStream = andThen(other) def andThen(other: HtmlStream): HtmlStream = HtmlStream(enumerator.andThen(other.enumerator)) } app/ui/HtmlStream.scala For streaming templates, we create an HtmlStream Appendable that wraps an Enumerator[Html].
    • object HtmlStream { def interleave(streams: HtmlStream*): HtmlStream = { HtmlStream(Enumerator.interleave(streams.map(_.enumerator))) } def flatten(eventuallyStream: Future[HtmlStream]): HtmlStream = { HtmlStream(Enumerator.flatten(eventuallyStream.map(_.enumerator))) } } app/ui/HtmlStream.scala We add an HtmlStream companion object with helper methods to combine HtmlStreams just like Enumerators
    • object HtmlStream { def apply(text: String): HtmlStream = { apply(Html(text)) } def apply(html: Html): HtmlStream = { HtmlStream(Enumerator(html)) } def apply(eventuallyHtml: Future[Html]): HtmlStream = { flatten(eventuallyHtml.map(apply)) } } app/ui/HtmlStream.scala Also, we add several methods to the HtmlStream companion object to help create HtmlStream instances.
    • trait Format[T <: Appendable[T]] { type Appendable = T def raw(text: String): T def escape(text: String): T } play/api/templates/Templates.scala The Format trait is what Play will use to create your Appendable from a String
    • object HtmlStreamFormat extends Format[HtmlStream] { def raw(text: String): HtmlStream = { HtmlStream(text) } def escape(text: String): HtmlStream = { raw(HtmlFormat.escape(text).body) } } app/ui/HtmlStream.scala Here’s the HtmlStreamFormat implementation
    • object HtmlStreamImplicits { // Implicit conversion so HtmlStream can be passed directly to Ok.feed and Ok.chunked implicit def toEnumerator(stream: HtmlStream): Enumerator[Html] = { // Skip empty chunks, as these mean EOF in chunked encoding stream.enumerator.through(Enumeratee.filter(!_.body.isEmpty)) } } app/ui/HtmlStream.scala We also include an implicit conversion so HtmlStream can be passed directly to Ok.chunked
    • @(body:ui.HtmlStream) <html> <head> <link rel="stylesheet" href="/assets/stylesheets/wvyp.css"/> </head> <body> <div class="wvyp"> <h2>Who's Viewed Your Profile</h2> @body </div> </body> </html> app/views/wvyp/wvyp.scala.stream We can now create a .scala.stream template that has markup mixed with HtmlStream elements.
    • object WVYPStream extends Controller { def index = Action { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") } } app/controllers/WVYPStream.scala Now for the streaming controller. As usual, start with the service calls.
    • object WVYPStream extends Controller { def index = Action { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") val wvypCountHtmlFuture = wvypCountFuture.map(str => views.html.wvyp.wvypCount(str.toInt)) val searchCountHtmlFuture = searchCountFuture.map(str => views.html.wvyp.searchCount(str.toInt)) } } app/controllers/WVYPStream.scala Next, render the data in each Future as Html
    • object WVYPStream extends Controller { def index = Action { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") val wvypCountHtmlFuture = wvypCountFuture.map(str => views.html.wvyp.wvypCount(str.toInt)) val searchCountHtmlFuture = searchCountFuture.map(str => views.html.wvyp.searchCount(str.toInt)) val wvypCountStream = HtmlStream(wvypCountHtmlFuture) val searchCountStream = HtmlStream(searchCountHtmlFuture) } } app/controllers/WVYPStream.scala Convert each Future[Html] to an HtmlStream using the factory methods we created earlier
    • object WVYPStream extends Controller { def index = Action { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") val wvypCountHtmlFuture = wvypCountFuture.map(str => views.html.wvyp.wvypCount(str.toInt)) val searchCountHtmlFuture = searchCountFuture.map(str => views.html.wvyp.searchCount(str.toInt)) val wvypCountStream = HtmlStream(wvypCountHtmlFuture) val searchCountStream = HtmlStream(searchCountHtmlFuture) val body = wvypCountStream.andThen(searchCountStream) Ok.chunked(views.stream.wvypStreaming(body)) } } app/controllers/WVYPStream.scala Finally, combine the streams using andThen and render the streaming template
    • GET @(wvypBody:/mock/:serviceName controllers.Mock.mock(serviceName: String) Html, wvyuBody: Html) GET /wvyp controllers.WVYP.index(embed: Boolean ?= false) GET <html> /wvyu controllers.WVYU.index(embed: Boolean ?= false) GET <head> /aggregate controllers.Aggregator.index GET <link rel="stylesheet" href="/assets/stylesheets/wvyp.css"/> /wvyp/enumerator controllers.WVYPEnumerator.index GET <link rel="stylesheet" href="/assets/stylesheets/wvyu.css"/> /wvyp/stream controllers.WVYPStream.index </head> <body> @wvypBody @wvyuBody </body> </html> app/views/aggregator/aggregator.scala Add a routes entry for the stream-based WVYP controller
    • The page loads almost immediately and we see the WVYP count right away.
    • 2 seconds later, the search count appears.
    • Also, the CSS starts downloading immediately!
    • This is a huge win if the stuff at the top of the page loads quickly. But what if it’s slow?
    • object Mock extends Controller { def mock(serviceName: String) = Action.async { serviceName match { case "wvyp" => respond(data = "56", delay = 2000) // SLOW! case "search" => respond(data = "10", delay = 20) case "likes" => respond(data = "150", delay = 40) case "comments" => respond(data = "14", delay = 20) } } private def respond(data: String, delay: Long) = Promise.timeout(Ok(data), delay) } app/mock/Mock.scala Modify the mock endpoint so wvyp is slow and search is fast
    • Again, the page loads almost immediately, but this time, neither count is visible.
    • 2 seconds later, both counts appear.
    • Fortunately, the CSS still loads right at the beginning.
    • So it’s still a big win, even if the top of the page is slow, but not by as much.
    • Is there a way to stream the data out of order but still render it in order?
    • JavaScript!
    • @(contents: Html, id: String) <script type="text/html-stream" id="@id-contents"> @contents </script> <script type="text/javascript"> document.getElementById("@id").innerHTML = document.getElementById("@id-contents").innerHTML; </script> app/views/ui/pagelet.scala.html Create a “pagelet” template that will take a snippet of HTML and use JS to inject it into the proper spot on the page.
    • object Pagelet { def render(html: Html, id: String): Html = { views.html.ui.pagelet(html, id) } def renderStream(html: Html, id: String): HtmlStream = { HtmlStream(render(html, id)) } def renderStream(htmlFuture: Future[Html], id: String): HtmlStream = { HtmlStream.flatten(htmlFuture.map(html => renderStream(html, id))) } } app/ui/Pagelet.scala Add a Pagelet class with helper methods to wrap Html and Future[Html] in the pagelet template.
    • @(body: ui.HtmlStream) <html> <head> <link rel="stylesheet" href="/assets/stylesheets/wvyp.css"/> </head> <body> <div class="wvyp"> <h2>Who's Viewed Your Profile</h2> <div id="wvypCount"></div> <div id="searchCount"></div> </div> @body </body> </html> app/views/wvyp/wvyp.scala.stream Update the WVYP streaming template to include placholder divs for the WVYP and search counts
    • def index = Action { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") val wvypCountHtmlFuture = wvypCountFuture.map(str => views.html.wvyp.wvypCount(str.toInt)) val searchCountHtmlFuture = searchCountFuture.map(str => views.html.wvyp.searchCount(str.toInt)) } app/controllers/WVYPStream.scala Finally, update the WVYPStream controller. We still make two service calls and render each as HTML.
    • def index = Action { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") val wvypCountHtmlFuture = wvypCountFuture.map(str => views.html.wvyp.wvypCount(str.toInt)) val searchCountHtmlFuture = searchCountFuture.map(str => views.html.wvyp.searchCount(str.toInt)) val wvypCountStream = Pagelet.renderStream(wvypCountHtmlFuture, "wvypCount") val searchCountStream = Pagelet.renderStream(searchCountHtmlFuture, "searchCount") } app/controllers/WVYPStream.scala This time, we convert the Future[Html] into an HtmlStream using the Pagelet.renderStream helper method.
    • def index = Action { val wvypCountFuture = ServiceClient.makeServiceCall("wvyp") val searchCountFuture = ServiceClient.makeServiceCall("search") val wvypCountHtmlFuture = wvypCountFuture.map(str => views.html.wvyp.wvypCount(str.toInt)) val searchCountHtmlFuture = searchCountFuture.map(str => views.html.wvyp.searchCount(str.toInt)) val wvypCountStream = Pagelet.renderStream(wvypCountHtmlFuture, "wvypCount") val searchCountStream = Pagelet.renderStream(searchCountHtmlFuture, "searchCount") val body = HtmlStream.interleave(wvypCountStream, searchCountStream) Ok.chunked(views.stream.wvypStreaming(body)) } app/controllers/WVYPStream.scala Instead of using andThen, we compose the streams using interleave. This way, the Html will stream as soon as it’s ready.
    • The page loads almost immediately, but now we see the search count first
    • 2 second later, the WVYP count appears.
    • We now have the basics for out-of-order, BigPipe style rendering!
    • Wins 1. Much faster time to first byte 2. Static content can start loading much earlier 3. User can see and start interacting with the page almost immediately 4. Out of order rendering with JavaScript allows even deeper optimizations
    • Caveats 1. Once you’ve streamed out the HTTP headers, you can’t change them. This means cookies and error handling may have to be done client-side. 2. Have to be careful with “pop in” and redraws. Client side code should intelligently choose when to insert items into the DOM. 3. Need to handle CSS and JS dependencies differently 4. Testing is harder 5. May need to disable JavaScript rendering for SEO
    • You can find the code from this presentation at: https://github.com/brikis98/ping-play
    • Thank you!