SlideShare a Scribd company logo
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
Composable and streamable Play apps
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
Composable and streamable Play apps
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!

More Related Content

PDF
Secure Spring Boot Microservices with Keycloak
PDF
Hive Bucketing in Apache Spark with Tejas Patil
PDF
Accelerating Data Ingestion with Databricks Autoloader
PDF
Native Java with GraalVM
PDF
Deep Dive Java 17 Devoxx UK
PDF
Elastic Cloud keynote
PPTX
PDF
KubeMonkey를 통한 Chaos Engineering 실전 운영하기 - 윤석찬 (AWS 테크에반젤리스트)
Secure Spring Boot Microservices with Keycloak
Hive Bucketing in Apache Spark with Tejas Patil
Accelerating Data Ingestion with Databricks Autoloader
Native Java with GraalVM
Deep Dive Java 17 Devoxx UK
Elastic Cloud keynote
KubeMonkey를 통한 Chaos Engineering 실전 운영하기 - 윤석찬 (AWS 테크에반젤리스트)

What's hot (20)

PPTX
Snowflake essentials
PDF
Oracle Business Intelligence Overview PPT
PPTX
Api Abstraction & Api Chaining
PDF
Spring Boot Actuator 2.0 & Micrometer
PPTX
Oracle APEX, Low Code for Data Driving Apps
PDF
Unified Stream and Batch Processing with Apache Flink
PDF
ACID ORC, Iceberg, and Delta Lake—An Overview of Table Formats for Large Scal...
PPTX
Implementing SRE practices: SLI/SLO deep dive - David Blank-Edelman - DevOpsD...
PDF
Stream Processing with Apache Kafka and .NET
PDF
Quarkus Denmark 2019
PDF
Models for hierarchical data
PPTX
Introduction to Azure Databricks
PDF
RxJS Evolved
PDF
Introduction to Apache Calcite
PDF
Deploying Flink on Kubernetes - David Anderson
PDF
Reactive Card Magic: Understanding Spring WebFlux and Project Reactor
PPTX
The openCypher Project - An Open Graph Query Language
PPTX
Zuul @ Netflix SpringOne Platform
PPTX
Backstage at CNCF Madison.pptx
PPTX
Json Web Token - JWT
Snowflake essentials
Oracle Business Intelligence Overview PPT
Api Abstraction & Api Chaining
Spring Boot Actuator 2.0 & Micrometer
Oracle APEX, Low Code for Data Driving Apps
Unified Stream and Batch Processing with Apache Flink
ACID ORC, Iceberg, and Delta Lake—An Overview of Table Formats for Large Scal...
Implementing SRE practices: SLI/SLO deep dive - David Blank-Edelman - DevOpsD...
Stream Processing with Apache Kafka and .NET
Quarkus Denmark 2019
Models for hierarchical data
Introduction to Azure Databricks
RxJS Evolved
Introduction to Apache Calcite
Deploying Flink on Kubernetes - David Anderson
Reactive Card Magic: Understanding Spring WebFlux and Project Reactor
The openCypher Project - An Open Graph Query Language
Zuul @ Netflix SpringOne Platform
Backstage at CNCF Madison.pptx
Json Web Token - JWT
Ad

Viewers also liked (20)

PDF
Play Framework: async I/O with Java and Scala
PPT
LinkedIn - A highly scalable Architecture on Java!
PDF
The Play Framework at LinkedIn
PPTX
An intro to Docker, Terraform, and Amazon ECS
PDF
Introduction to Iteratees (Scala)
PDF
Node.js vs Play Framework (with Japanese subtitles)
PPTX
Infrastructure as code: running microservices on AWS using Docker, Terraform,...
PPTX
Iteratee and stream with Play2 scala
PDF
Data Workflows for Machine Learning - SF Bay Area ML
PPTX
From Data to Decisions Makers: A Behind the Scenes Look at Building The Most ...
PDF
Chicago Hadoop Users Group: Enterprise Data Workflows
PDF
Spring 3.1 and MVC Testing Support - 4Developers
PDF
Reactive Programming With Akka - Lessons Learned
PDF
The no-framework Scala Dependency Injection Framework
PDF
A Sceptical Guide to Functional Programming
PDF
Actor Based Asyncronous IO in Akka
PDF
Effective akka scalaio
PDF
DUMP-2015: «Распределенная обработка миллионов документов на Scala и Akka» Ст...
PDF
Playing with Scala
PDF
[Start] Playing
Play Framework: async I/O with Java and Scala
LinkedIn - A highly scalable Architecture on Java!
The Play Framework at LinkedIn
An intro to Docker, Terraform, and Amazon ECS
Introduction to Iteratees (Scala)
Node.js vs Play Framework (with Japanese subtitles)
Infrastructure as code: running microservices on AWS using Docker, Terraform,...
Iteratee and stream with Play2 scala
Data Workflows for Machine Learning - SF Bay Area ML
From Data to Decisions Makers: A Behind the Scenes Look at Building The Most ...
Chicago Hadoop Users Group: Enterprise Data Workflows
Spring 3.1 and MVC Testing Support - 4Developers
Reactive Programming With Akka - Lessons Learned
The no-framework Scala Dependency Injection Framework
A Sceptical Guide to Functional Programming
Actor Based Asyncronous IO in Akka
Effective akka scalaio
DUMP-2015: «Распределенная обработка миллионов документов на Scala и Akka» Ст...
Playing with Scala
[Start] Playing
Ad

Similar to Composable and streamable Play apps (20)

PDF
Server side rendering with React and Symfony
PDF
Angular resolver tutorial
PDF
Itb 2021 - Bulding Quick APIs by Gavin Pickin
PPTX
MongoDB World 2018: Ch-Ch-Ch-Ch-Changes: Taking Your Stitch Application to th...
PPTX
Sherlock Homepage - A detective story about running large web services - NDC ...
PDF
Behind the scenes of Scaleway Functions : when Kubernetes meets our products
PDF
"Service Worker: Let Your Web App Feel Like a Native "
PPT
Java servlet life cycle - methods ppt
PDF
Play 23x Documentation For Scala Developers 23x Unknown
PDF
using Mithril.js + postgREST to build and consume API's
PPTX
Sherlock Homepage - A detective story about running large web services (VISUG...
PPTX
Sherlock Homepage (Maarten Balliauw)
PDF
Rethinking Syncing at AltConf 2019
PDF
Into The Box | Alexa and ColdBox Api's
PDF
What's new in Rails 5 - API Mode & Action Cable overview
PPTX
Battle of React State Managers in frontend applications
PDF
Functional UIs with Java 8 and Vaadin JavaOne2014
PPT
Elefrant [ng-Poznan]
PDF
Active object of Symbian in the lights of client server architecture
ODP
Http programming in play
Server side rendering with React and Symfony
Angular resolver tutorial
Itb 2021 - Bulding Quick APIs by Gavin Pickin
MongoDB World 2018: Ch-Ch-Ch-Ch-Changes: Taking Your Stitch Application to th...
Sherlock Homepage - A detective story about running large web services - NDC ...
Behind the scenes of Scaleway Functions : when Kubernetes meets our products
"Service Worker: Let Your Web App Feel Like a Native "
Java servlet life cycle - methods ppt
Play 23x Documentation For Scala Developers 23x Unknown
using Mithril.js + postgREST to build and consume API's
Sherlock Homepage - A detective story about running large web services (VISUG...
Sherlock Homepage (Maarten Balliauw)
Rethinking Syncing at AltConf 2019
Into The Box | Alexa and ColdBox Api's
What's new in Rails 5 - API Mode & Action Cable overview
Battle of React State Managers in frontend applications
Functional UIs with Java 8 and Vaadin JavaOne2014
Elefrant [ng-Poznan]
Active object of Symbian in the lights of client server architecture
Http programming in play

More from Yevgeniy Brikman (18)

PDF
Cloud adoption fails - 5 ways deployments go wrong and 5 solutions
PDF
How to test infrastructure code: automated testing for Terraform, Kubernetes,...
PDF
Lessons learned from writing over 300,000 lines of infrastructure code
PPTX
Gruntwork Executive Summary
PPTX
Reusable, composable, battle-tested Terraform modules
PPTX
The Truth About Startups: What I wish someone had told me about entrepreneurs...
PPTX
Comprehensive Terraform Training
PPTX
Agility Requires Safety
PPTX
Startup Ideas and Validation
PPTX
A Guide to Hiring for your Startup
PDF
Startup DNA: Speed Wins
PDF
Node.js vs Play Framework
PDF
Rapid prototyping
PDF
Kings of Code Hack Battle
PDF
Hackdays and [in]cubator
PPTX
Startup DNA: the formula behind successful startups in Silicon Valley (update...
PDF
PDF
LinkedIn Overview
Cloud adoption fails - 5 ways deployments go wrong and 5 solutions
How to test infrastructure code: automated testing for Terraform, Kubernetes,...
Lessons learned from writing over 300,000 lines of infrastructure code
Gruntwork Executive Summary
Reusable, composable, battle-tested Terraform modules
The Truth About Startups: What I wish someone had told me about entrepreneurs...
Comprehensive Terraform Training
Agility Requires Safety
Startup Ideas and Validation
A Guide to Hiring for your Startup
Startup DNA: Speed Wins
Node.js vs Play Framework
Rapid prototyping
Kings of Code Hack Battle
Hackdays and [in]cubator
Startup DNA: the formula behind successful startups in Silicon Valley (update...
LinkedIn Overview

Recently uploaded (20)

PDF
Shreyas Phanse Resume: Experienced Backend Engineer | Java • Spring Boot • Ka...
PDF
Spectral efficient network and resource selection model in 5G networks
PPTX
20250228 LYD VKU AI Blended-Learning.pptx
PDF
Building Integrated photovoltaic BIPV_UPV.pdf
PDF
Reach Out and Touch Someone: Haptics and Empathic Computing
PDF
Per capita expenditure prediction using model stacking based on satellite ima...
PDF
Advanced methodologies resolving dimensionality complications for autism neur...
PDF
Dropbox Q2 2025 Financial Results & Investor Presentation
PDF
How UI/UX Design Impacts User Retention in Mobile Apps.pdf
PDF
NewMind AI Monthly Chronicles - July 2025
PDF
The Rise and Fall of 3GPP – Time for a Sabbatical?
PDF
Peak of Data & AI Encore- AI for Metadata and Smarter Workflows
PDF
Diabetes mellitus diagnosis method based random forest with bat algorithm
PDF
cuic standard and advanced reporting.pdf
PPTX
VMware vSphere Foundation How to Sell Presentation-Ver1.4-2-14-2024.pptx
PDF
Review of recent advances in non-invasive hemoglobin estimation
PDF
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
PDF
NewMind AI Weekly Chronicles - August'25 Week I
PDF
Encapsulation theory and applications.pdf
PDF
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
Shreyas Phanse Resume: Experienced Backend Engineer | Java • Spring Boot • Ka...
Spectral efficient network and resource selection model in 5G networks
20250228 LYD VKU AI Blended-Learning.pptx
Building Integrated photovoltaic BIPV_UPV.pdf
Reach Out and Touch Someone: Haptics and Empathic Computing
Per capita expenditure prediction using model stacking based on satellite ima...
Advanced methodologies resolving dimensionality complications for autism neur...
Dropbox Q2 2025 Financial Results & Investor Presentation
How UI/UX Design Impacts User Retention in Mobile Apps.pdf
NewMind AI Monthly Chronicles - July 2025
The Rise and Fall of 3GPP – Time for a Sabbatical?
Peak of Data & AI Encore- AI for Metadata and Smarter Workflows
Diabetes mellitus diagnosis method based random forest with bat algorithm
cuic standard and advanced reporting.pdf
VMware vSphere Foundation How to Sell Presentation-Ver1.4-2-14-2024.pptx
Review of recent advances in non-invasive hemoglobin estimation
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
NewMind AI Weekly Chronicles - August'25 Week I
Encapsulation theory and applications.pdf
Build a system with the filesystem maintained by OSTree @ COSCUP 2025

Composable and streamable Play apps

  • 2. About me Part of the Service Infrastructure team, which is bringing the Play Framework to LinkedIn engineers.
  • 3. At LinkedIn, we’ve been running Play apps in production for over a year
  • 4. More than 60 apps on Play now, including:
  • 9. Play Labs (e.g. InDemand)
  • 10. Many internal tools (e.g. REST search)
  • 11. Plus many backends services that don’t have sexy screenshots
  • 12. We love Play… but we’ve had a couple tiny problems:
  • 13. We love Play… but we’ve had a couple tiny problems: 1. Unmanageable complexity
  • 14. We love Play… but we’ve had a couple tiny problems: 1. Unmanageable complexity 2. Terrible performance
  • 15. It’s probably not what you think.
  • 16. This talk is about a couple techniques to deal with complexity and performance problems.
  • 17. These techniques are experimental.
  • 20. You can find the code from this presentation at: https://github.com/brikis98/ping-play
  • 21. Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
  • 22. Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
  • 23. Web pages can get complex
  • 24. For example, consider the LinkedIn home page
  • 25. Almost every part of the page is dynamic, highly customized for the user, and interactive.
  • 26. Trying to cram this all into one controller and one template is completely unmaintainable
  • 27. “Managing complexity is the most important technical topic in software development.” Steve McConnell, Code Complete
  • 29. “Abstraction is the ability to engage with a concept while safely ignoring some of its details.” Steve McConnell, Code Complete
  • 30. Abstraction allows you to take a simpler view of a complex concept
  • 31. Composition: assemble complex behavior by combining simpler behavior
  • 33. Abstraction: structure your app so you can focus on one part while safely ignoring the rest
  • 34. 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
  • 35. Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
  • 36. We’ll start by building a completely standalone “Who’s Viewed Your Profile” (WVYP) module
  • 37. We need: 1. Data: WVYP count, search count 2. Template: to render the HTML 3. Controller: to tie it all together
  • 39. For this talk, we’re going to simulate service calls by calling a mock endpoint
  • 40. 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.
  • 42. 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
  • 43. @(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.
  • 44. @(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
  • 45. 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.
  • 46. 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.
  • 49. We now have one small module
  • 50. Now, imagine we also have another standalone module called “Who’s Viewed Your Updates” (WVYU)
  • 52. How can we combine the two modules?
  • 53. trait Action[A] extends EssentialAction { def apply(request: Request[A]): Future[SimpleResult] } play/api/mvc/Action.scala In Play, an Action is a function
  • 54. Request[A] => Future[SimpleResult] The actions in each standalone module are just functions from Request to Result. Functions can be composed!
  • 55. @(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!
  • 56. 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.
  • 57. 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
  • 58. 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.
  • 59. 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].
  • 60. 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
  • 61. 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
  • 62. 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
  • 63. The result. We’ve composed two Play modules!
  • 64. 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.
  • 65. 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.
  • 66. Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
  • 67. The composition code glosses over a few details
  • 68. Some questions: 1. How do we set cookies? 2. How do we handle errors? 3. How do we aggregate static content?
  • 69. It turns out HTTP is an elegant way to answer these questions.
  • 70. 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
  • 71. 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!
  • 72. 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
  • 73. 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
  • 74. We can see the aggregate endpoint is now setting cookies
  • 75. HTTP headers are a good way to handle error cases too.
  • 76. A quick review of HTTP status codes
  • 77. For example, if a module has no content, return a 204
  • 78. Invalid module request? Return 404.
  • 79. Plus other important status codes
  • 81. 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
  • 82. CSS and JS dependencies can be set as a header and aggregated too.
  • 83. 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
  • 84. 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.
  • 85. 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
  • 86. 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
  • 87. 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
  • 88. Caveats 1. Managing static content in a controller, instead of a view, feels clunky.
  • 89. Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
  • 90. So far, we’ve been using a mock endpoint to simulate remote service calls
  • 91. What happens if one of the remote calls is slow?
  • 92. 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
  • 93. Time to first byte is 2 seconds! Everything has to wait for the one slow service, including static content.
  • 94. Is there a way to start sending the response before all the data is available?
  • 95. Facebook solves this problem with BigPipe https://www.facebook.com/note.php?note_id=389414033919
  • 96. Can we build BigPipe with Play?
  • 97. (Well, yea, or I wouldn’t have made this talk)
  • 98. We can use Enumerators!
  • 99. Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
  • 100. Enumerators are part of the Play Iteratees library.
  • 101. 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.
  • 103. These are all composable abstractions for working with streams of data
  • 104. Let’s look at some examples
  • 105. 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
  • 106. 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.
  • 107. Basic “Hello World” example using Enumerator.apply.
  • 108. 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
  • 109. The word “Hello” is pumped out every 500ms. Note: when testing streaming, use “curl -N” so curl doesn’t buffer.
  • 110. 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.
  • 111. With andThen, we see all the data from the first enumerator and then all the data from the second one
  • 112. 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
  • 113. With interleave, data can come in any order. Above, we see “Hello” every 500ms and “Goodbye” every 1000ms.
  • 114. Let’s use Enumerators to stream the results of our remote service calls
  • 115. 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.
  • 116. 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]
  • 117. 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
  • 118. 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
  • 119. Almost immediately, we see “56”, from the wvyp service.
  • 120. 2 seconds later, we see “10”, from the search service.
  • 122. However, we’re only streaming plain text. How do we stream a template?
  • 123. Outline 1. Complexity 2. Composition 3. HTTP 4. Performance 5. Enumerators 6. Streaming
  • 124. 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.
  • 125. The new template type must define an “Appendable” and a “Format” for it
  • 126. trait Appendable[T] { def +=(other: T): T } play/api/templates/Templates.scala The Appendable trait is simple: it’s anything with an append (+=) operator.
  • 127. 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.
  • 128. 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].
  • 129. 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
  • 130. 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.
  • 131. 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
  • 132. 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
  • 133. 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
  • 134. @(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.
  • 135. 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.
  • 136. 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
  • 137. 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
  • 138. 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
  • 139. 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
  • 140. The page loads almost immediately and we see the WVYP count right away.
  • 141. 2 seconds later, the search count appears.
  • 142. Also, the CSS starts downloading immediately!
  • 143. This is a huge win if the stuff at the top of the page loads quickly. But what if it’s slow?
  • 144. 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
  • 145. Again, the page loads almost immediately, but this time, neither count is visible.
  • 146. 2 seconds later, both counts appear.
  • 147. Fortunately, the CSS still loads right at the beginning.
  • 148. So it’s still a big win, even if the top of the page is slow, but not by as much.
  • 149. Is there a way to stream the data out of order but still render it in order?
  • 151. @(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.
  • 152. 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.
  • 153. @(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
  • 154. 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.
  • 155. 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.
  • 156. 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.
  • 157. The page loads almost immediately, but now we see the search count first
  • 158. 2 second later, the WVYP count appears.
  • 159. We now have the basics for out-of-order, BigPipe style rendering!
  • 160. 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
  • 161. 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
  • 162. You can find the code from this presentation at: https://github.com/brikis98/ping-play