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
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!

More Related Content

What's hot

Deep Dive Java 17 Devoxx UK
Deep Dive Java 17 Devoxx UKDeep Dive Java 17 Devoxx UK
Deep Dive Java 17 Devoxx UK
José Paumard
 
Building a Kubernetes App with Amazon EKS
Building a Kubernetes App with Amazon EKSBuilding a Kubernetes App with Amazon EKS
Building a Kubernetes App with Amazon EKS
DevOps.com
 
Getting Started with Infrastructure as Code
Getting Started with Infrastructure as CodeGetting Started with Infrastructure as Code
Getting Started with Infrastructure as Code
WinWire Technologies Inc
 
Service workers
Service workersService workers
Service workers
jungkees
 
Modern JS with ES6
Modern JS with ES6Modern JS with ES6
Modern JS with ES6
Kevin Langley Jr.
 
[112]rest에서 graph ql과 relay로 갈아타기 이정우
[112]rest에서 graph ql과 relay로 갈아타기 이정우[112]rest에서 graph ql과 relay로 갈아타기 이정우
[112]rest에서 graph ql과 relay로 갈아타기 이정우
NAVER D2
 
Reusable, composable, battle-tested Terraform modules
Reusable, composable, battle-tested Terraform modulesReusable, composable, battle-tested Terraform modules
Reusable, composable, battle-tested Terraform modules
Yevgeniy Brikman
 
TestNG Annotations in Selenium | Edureka
TestNG Annotations in Selenium | EdurekaTestNG Annotations in Selenium | Edureka
TestNG Annotations in Selenium | Edureka
Edureka!
 
Lombok
LombokLombok
AWS Elastic Beanstalk
AWS Elastic BeanstalkAWS Elastic Beanstalk
AWS Elastic Beanstalk
Amazon Web Services
 
Comprehensive Terraform Training
Comprehensive Terraform TrainingComprehensive Terraform Training
Comprehensive Terraform Training
Yevgeniy Brikman
 
Masterclass Webinar - AWS CloudFormation
Masterclass Webinar - AWS CloudFormationMasterclass Webinar - AWS CloudFormation
Masterclass Webinar - AWS CloudFormation
Amazon Web Services
 
AWS CloudFormation Masterclass
AWS CloudFormation MasterclassAWS CloudFormation Masterclass
AWS CloudFormation Masterclass
Amazon Web Services
 
ES6 presentation
ES6 presentationES6 presentation
ES6 presentation
ritika1
 
Adapt or Die: A Microservices Story at Google
Adapt or Die: A Microservices Story at GoogleAdapt or Die: A Microservices Story at Google
Adapt or Die: A Microservices Story at Google
Apigee | Google Cloud
 
SwiftUI For Production | try! Swift 2019
SwiftUI For Production | try! Swift 2019SwiftUI For Production | try! Swift 2019
SwiftUI For Production | try! Swift 2019
Lea Marolt Sonnenschein
 
Vue js for beginner
Vue js for beginner Vue js for beginner
Vue js for beginner
Chandrasekar G
 
JavaScript Promises
JavaScript PromisesJavaScript Promises
JavaScript Promises
L&T Technology Services Limited
 
Deep Dive: AWS CloudFormation
Deep Dive: AWS CloudFormationDeep Dive: AWS CloudFormation
Deep Dive: AWS CloudFormation
Amazon Web Services
 
Lessons learned from writing over 300,000 lines of infrastructure code
Lessons learned from writing over 300,000 lines of infrastructure codeLessons learned from writing over 300,000 lines of infrastructure code
Lessons learned from writing over 300,000 lines of infrastructure code
Yevgeniy Brikman
 

What's hot (20)

Deep Dive Java 17 Devoxx UK
Deep Dive Java 17 Devoxx UKDeep Dive Java 17 Devoxx UK
Deep Dive Java 17 Devoxx UK
 
Building a Kubernetes App with Amazon EKS
Building a Kubernetes App with Amazon EKSBuilding a Kubernetes App with Amazon EKS
Building a Kubernetes App with Amazon EKS
 
Getting Started with Infrastructure as Code
Getting Started with Infrastructure as CodeGetting Started with Infrastructure as Code
Getting Started with Infrastructure as Code
 
Service workers
Service workersService workers
Service workers
 
Modern JS with ES6
Modern JS with ES6Modern JS with ES6
Modern JS with ES6
 
[112]rest에서 graph ql과 relay로 갈아타기 이정우
[112]rest에서 graph ql과 relay로 갈아타기 이정우[112]rest에서 graph ql과 relay로 갈아타기 이정우
[112]rest에서 graph ql과 relay로 갈아타기 이정우
 
Reusable, composable, battle-tested Terraform modules
Reusable, composable, battle-tested Terraform modulesReusable, composable, battle-tested Terraform modules
Reusable, composable, battle-tested Terraform modules
 
TestNG Annotations in Selenium | Edureka
TestNG Annotations in Selenium | EdurekaTestNG Annotations in Selenium | Edureka
TestNG Annotations in Selenium | Edureka
 
Lombok
LombokLombok
Lombok
 
AWS Elastic Beanstalk
AWS Elastic BeanstalkAWS Elastic Beanstalk
AWS Elastic Beanstalk
 
Comprehensive Terraform Training
Comprehensive Terraform TrainingComprehensive Terraform Training
Comprehensive Terraform Training
 
Masterclass Webinar - AWS CloudFormation
Masterclass Webinar - AWS CloudFormationMasterclass Webinar - AWS CloudFormation
Masterclass Webinar - AWS CloudFormation
 
AWS CloudFormation Masterclass
AWS CloudFormation MasterclassAWS CloudFormation Masterclass
AWS CloudFormation Masterclass
 
ES6 presentation
ES6 presentationES6 presentation
ES6 presentation
 
Adapt or Die: A Microservices Story at Google
Adapt or Die: A Microservices Story at GoogleAdapt or Die: A Microservices Story at Google
Adapt or Die: A Microservices Story at Google
 
SwiftUI For Production | try! Swift 2019
SwiftUI For Production | try! Swift 2019SwiftUI For Production | try! Swift 2019
SwiftUI For Production | try! Swift 2019
 
Vue js for beginner
Vue js for beginner Vue js for beginner
Vue js for beginner
 
JavaScript Promises
JavaScript PromisesJavaScript Promises
JavaScript Promises
 
Deep Dive: AWS CloudFormation
Deep Dive: AWS CloudFormationDeep Dive: AWS CloudFormation
Deep Dive: AWS CloudFormation
 
Lessons learned from writing over 300,000 lines of infrastructure code
Lessons learned from writing over 300,000 lines of infrastructure codeLessons learned from writing over 300,000 lines of infrastructure code
Lessons learned from writing over 300,000 lines of infrastructure code
 

Viewers also liked

Play Framework: async I/O with Java and Scala
Play Framework: async I/O with Java and ScalaPlay Framework: async I/O with Java and Scala
Play Framework: async I/O with Java and Scala
Yevgeniy Brikman
 
LinkedIn - A highly scalable Architecture on Java!
LinkedIn - A highly scalable Architecture on Java!LinkedIn - A highly scalable Architecture on Java!
LinkedIn - A highly scalable Architecture on Java!
manivannan57
 
The Play Framework at LinkedIn
The Play Framework at LinkedInThe Play Framework at LinkedIn
The Play Framework at LinkedIn
Yevgeniy Brikman
 
An intro to Docker, Terraform, and Amazon ECS
An intro to Docker, Terraform, and Amazon ECSAn intro to Docker, Terraform, and Amazon ECS
An intro to Docker, Terraform, and Amazon ECS
Yevgeniy Brikman
 
Introduction to Iteratees (Scala)
Introduction to Iteratees (Scala)Introduction to Iteratees (Scala)
Introduction to Iteratees (Scala)
Alexander Lehmann
 
Node.js vs Play Framework (with Japanese subtitles)
Node.js vs Play Framework (with Japanese subtitles)Node.js vs Play Framework (with Japanese subtitles)
Node.js vs Play Framework (with Japanese subtitles)
Yevgeniy Brikman
 
Infrastructure as code: running microservices on AWS using Docker, Terraform,...
Infrastructure as code: running microservices on AWS using Docker, Terraform,...Infrastructure as code: running microservices on AWS using Docker, Terraform,...
Infrastructure as code: running microservices on AWS using Docker, Terraform,...
Yevgeniy Brikman
 
Iteratee and stream with Play2 scala
Iteratee and stream with Play2 scalaIteratee and stream with Play2 scala
Iteratee and stream with Play2 scala
Quentin Adam
 
Data Workflows for Machine Learning - SF Bay Area ML
Data Workflows for Machine Learning - SF Bay Area MLData Workflows for Machine Learning - SF Bay Area ML
Data Workflows for Machine Learning - SF Bay Area ML
Paco Nathan
 
From Data to Decisions Makers: A Behind the Scenes Look at Building The Most ...
From Data to Decisions Makers: A Behind the Scenes Look at Building The Most ...From Data to Decisions Makers: A Behind the Scenes Look at Building The Most ...
From Data to Decisions Makers: A Behind the Scenes Look at Building The Most ...
Bob Rudis
 
Chicago Hadoop Users Group: Enterprise Data Workflows
Chicago Hadoop Users Group: Enterprise Data WorkflowsChicago Hadoop Users Group: Enterprise Data Workflows
Chicago Hadoop Users Group: Enterprise Data Workflows
Paco Nathan
 
Spring 3.1 and MVC Testing Support - 4Developers
Spring 3.1 and MVC Testing Support - 4DevelopersSpring 3.1 and MVC Testing Support - 4Developers
Spring 3.1 and MVC Testing Support - 4Developers
Sam Brannen
 
Reactive Programming With Akka - Lessons Learned
Reactive Programming With Akka - Lessons LearnedReactive Programming With Akka - Lessons Learned
Reactive Programming With Akka - Lessons Learned
Daniel Sawano
 
The no-framework Scala Dependency Injection Framework
The no-framework Scala Dependency Injection FrameworkThe no-framework Scala Dependency Injection Framework
The no-framework Scala Dependency Injection Framework
Adam Warski
 
A Sceptical Guide to Functional Programming
A Sceptical Guide to Functional ProgrammingA Sceptical Guide to Functional Programming
A Sceptical Guide to Functional Programming
Garth Gilmour
 
Actor Based Asyncronous IO in Akka
Actor Based Asyncronous IO in AkkaActor Based Asyncronous IO in Akka
Actor Based Asyncronous IO in Akka
drewhk
 
Effective akka scalaio
Effective akka scalaioEffective akka scalaio
Effective akka scalaio
shinolajla
 
DUMP-2015: «Распределенная обработка миллионов документов на Scala и Akka» Ст...
DUMP-2015: «Распределенная обработка миллионов документов на Scala и Akka» Ст...DUMP-2015: «Распределенная обработка миллионов документов на Scala и Akka» Ст...
DUMP-2015: «Распределенная обработка миллионов документов на Scala и Akka» Ст...
it-people
 
Playing with Scala
Playing with ScalaPlaying with Scala
Playing with Scala
Tamer Abdul-Radi
 
[Start] Playing
[Start] Playing[Start] Playing
[Start] Playing
佑介 九岡
 

Viewers also liked (20)

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

Similar to Composable and streamable Play apps

Server side rendering with React and Symfony
Server side rendering with React and SymfonyServer side rendering with React and Symfony
Server side rendering with React and Symfony
Ignacio Martín
 
Angular resolver tutorial
Angular resolver tutorialAngular resolver tutorial
Angular resolver tutorial
Katy Slemon
 
Itb 2021 - Bulding Quick APIs by Gavin Pickin
Itb 2021 - Bulding Quick APIs by Gavin PickinItb 2021 - Bulding Quick APIs by Gavin Pickin
Itb 2021 - Bulding Quick APIs by Gavin Pickin
Gavin Pickin
 
MongoDB World 2018: Ch-Ch-Ch-Ch-Changes: Taking Your Stitch Application to th...
MongoDB World 2018: Ch-Ch-Ch-Ch-Changes: Taking Your Stitch Application to th...MongoDB World 2018: Ch-Ch-Ch-Ch-Changes: Taking Your Stitch Application to th...
MongoDB World 2018: Ch-Ch-Ch-Ch-Changes: Taking Your Stitch Application to th...
MongoDB
 
Sherlock Homepage - A detective story about running large web services - NDC ...
Sherlock Homepage - A detective story about running large web services - NDC ...Sherlock Homepage - A detective story about running large web services - NDC ...
Sherlock Homepage - A detective story about running large web services - NDC ...
Maarten Balliauw
 
Behind the scenes of Scaleway Functions : when Kubernetes meets our products
Behind the scenes of Scaleway Functions : when Kubernetes meets our productsBehind the scenes of Scaleway Functions : when Kubernetes meets our products
Behind the scenes of Scaleway Functions : when Kubernetes meets our products
Scaleway
 
"Service Worker: Let Your Web App Feel Like a Native "
"Service Worker: Let Your Web App Feel Like a Native ""Service Worker: Let Your Web App Feel Like a Native "
"Service Worker: Let Your Web App Feel Like a Native "
FDConf
 
Java servlet life cycle - methods ppt
Java servlet life cycle - methods pptJava servlet life cycle - methods ppt
Java servlet life cycle - methods ppt
kamal kotecha
 
using Mithril.js + postgREST to build and consume API's
using Mithril.js + postgREST to build and consume API'susing Mithril.js + postgREST to build and consume API's
using Mithril.js + postgREST to build and consume API's
Antônio Roberto Silva
 
Sherlock Homepage (Maarten Balliauw)
Sherlock Homepage (Maarten Balliauw)Sherlock Homepage (Maarten Balliauw)
Sherlock Homepage (Maarten Balliauw)
Visug
 
Sherlock Homepage - A detective story about running large web services (VISUG...
Sherlock Homepage - A detective story about running large web services (VISUG...Sherlock Homepage - A detective story about running large web services (VISUG...
Sherlock Homepage - A detective story about running large web services (VISUG...
Maarten Balliauw
 
Rethinking Syncing at AltConf 2019
Rethinking Syncing at AltConf 2019Rethinking Syncing at AltConf 2019
Rethinking Syncing at AltConf 2019
Joe Keeley
 
Into The Box | Alexa and ColdBox Api's
Into The Box | Alexa and ColdBox Api'sInto The Box | Alexa and ColdBox Api's
Into The Box | Alexa and ColdBox Api's
Ortus Solutions, Corp
 
What's new in Rails 5 - API Mode & Action Cable overview
What's new in Rails 5 - API Mode & Action Cable overviewWhat's new in Rails 5 - API Mode & Action Cable overview
What's new in Rails 5 - API Mode & Action Cable overview
Maxim Veksler
 
Battle of React State Managers in frontend applications
Battle of React State Managers in frontend applicationsBattle of React State Managers in frontend applications
Battle of React State Managers in frontend applications
Evangelia Mitsopoulou
 
Functional UIs with Java 8 and Vaadin JavaOne2014
Functional UIs with Java 8 and Vaadin JavaOne2014Functional UIs with Java 8 and Vaadin JavaOne2014
Functional UIs with Java 8 and Vaadin JavaOne2014
hezamu
 
Elefrant [ng-Poznan]
Elefrant [ng-Poznan]Elefrant [ng-Poznan]
Elefrant [ng-Poznan]
Marcos Latorre
 
Active object of Symbian in the lights of client server architecture
Active object of Symbian in the lights of client server architectureActive object of Symbian in the lights of client server architecture
Active object of Symbian in the lights of client server architecture
Somenath Mukhopadhyay
 
Http programming in play
Http programming in playHttp programming in play
Http programming in play
Knoldus Inc.
 
Mvc interview questions – deep dive jinal desai
Mvc interview questions – deep dive   jinal desaiMvc interview questions – deep dive   jinal desai
Mvc interview questions – deep dive jinal desai
jinaldesailive
 

Similar to Composable and streamable Play apps (20)

Server side rendering with React and Symfony
Server side rendering with React and SymfonyServer side rendering with React and Symfony
Server side rendering with React and Symfony
 
Angular resolver tutorial
Angular resolver tutorialAngular resolver tutorial
Angular resolver tutorial
 
Itb 2021 - Bulding Quick APIs by Gavin Pickin
Itb 2021 - Bulding Quick APIs by Gavin PickinItb 2021 - Bulding Quick APIs by Gavin Pickin
Itb 2021 - Bulding Quick APIs by Gavin Pickin
 
MongoDB World 2018: Ch-Ch-Ch-Ch-Changes: Taking Your Stitch Application to th...
MongoDB World 2018: Ch-Ch-Ch-Ch-Changes: Taking Your Stitch Application to th...MongoDB World 2018: Ch-Ch-Ch-Ch-Changes: Taking Your Stitch Application to th...
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 ...
Sherlock Homepage - A detective story about running large web services - NDC ...Sherlock Homepage - A detective story about running large web services - NDC ...
Sherlock Homepage - A detective story about running large web services - NDC ...
 
Behind the scenes of Scaleway Functions : when Kubernetes meets our products
Behind the scenes of Scaleway Functions : when Kubernetes meets our productsBehind the scenes of Scaleway Functions : when Kubernetes meets our products
Behind the scenes of Scaleway Functions : when Kubernetes meets our products
 
"Service Worker: Let Your Web App Feel Like a Native "
"Service Worker: Let Your Web App Feel Like a Native ""Service Worker: Let Your Web App Feel Like a Native "
"Service Worker: Let Your Web App Feel Like a Native "
 
Java servlet life cycle - methods ppt
Java servlet life cycle - methods pptJava servlet life cycle - methods ppt
Java servlet life cycle - methods ppt
 
using Mithril.js + postgREST to build and consume API's
using Mithril.js + postgREST to build and consume API'susing Mithril.js + postgREST to build and consume API's
using Mithril.js + postgREST to build and consume API's
 
Sherlock Homepage (Maarten Balliauw)
Sherlock Homepage (Maarten Balliauw)Sherlock Homepage (Maarten Balliauw)
Sherlock Homepage (Maarten Balliauw)
 
Sherlock Homepage - A detective story about running large web services (VISUG...
Sherlock Homepage - A detective story about running large web services (VISUG...Sherlock Homepage - A detective story about running large web services (VISUG...
Sherlock Homepage - A detective story about running large web services (VISUG...
 
Rethinking Syncing at AltConf 2019
Rethinking Syncing at AltConf 2019Rethinking Syncing at AltConf 2019
Rethinking Syncing at AltConf 2019
 
Into The Box | Alexa and ColdBox Api's
Into The Box | Alexa and ColdBox Api'sInto The Box | Alexa and ColdBox Api's
Into The Box | Alexa and ColdBox Api's
 
What's new in Rails 5 - API Mode & Action Cable overview
What's new in Rails 5 - API Mode & Action Cable overviewWhat's new in Rails 5 - API Mode & Action Cable overview
What's new in Rails 5 - API Mode & Action Cable overview
 
Battle of React State Managers in frontend applications
Battle of React State Managers in frontend applicationsBattle of React State Managers in frontend applications
Battle of React State Managers in frontend applications
 
Functional UIs with Java 8 and Vaadin JavaOne2014
Functional UIs with Java 8 and Vaadin JavaOne2014Functional UIs with Java 8 and Vaadin JavaOne2014
Functional UIs with Java 8 and Vaadin JavaOne2014
 
Elefrant [ng-Poznan]
Elefrant [ng-Poznan]Elefrant [ng-Poznan]
Elefrant [ng-Poznan]
 
Active object of Symbian in the lights of client server architecture
Active object of Symbian in the lights of client server architectureActive object of Symbian in the lights of client server architecture
Active object of Symbian in the lights of client server architecture
 
Http programming in play
Http programming in playHttp programming in play
Http programming in play
 
Mvc interview questions – deep dive jinal desai
Mvc interview questions – deep dive   jinal desaiMvc interview questions – deep dive   jinal desai
Mvc interview questions – deep dive jinal desai
 

More from Yevgeniy Brikman

Cloud adoption fails - 5 ways deployments go wrong and 5 solutions
Cloud adoption fails - 5 ways deployments go wrong and 5 solutionsCloud adoption fails - 5 ways deployments go wrong and 5 solutions
Cloud adoption fails - 5 ways deployments go wrong and 5 solutions
Yevgeniy Brikman
 
How to test infrastructure code: automated testing for Terraform, Kubernetes,...
How to test infrastructure code: automated testing for Terraform, Kubernetes,...How to test infrastructure code: automated testing for Terraform, Kubernetes,...
How to test infrastructure code: automated testing for Terraform, Kubernetes,...
Yevgeniy Brikman
 
Gruntwork Executive Summary
Gruntwork Executive SummaryGruntwork Executive Summary
Gruntwork Executive Summary
Yevgeniy Brikman
 
The Truth About Startups: What I wish someone had told me about entrepreneurs...
The Truth About Startups: What I wish someone had told me about entrepreneurs...The Truth About Startups: What I wish someone had told me about entrepreneurs...
The Truth About Startups: What I wish someone had told me about entrepreneurs...
Yevgeniy Brikman
 
Agility Requires Safety
Agility Requires SafetyAgility Requires Safety
Agility Requires Safety
Yevgeniy Brikman
 
Startup Ideas and Validation
Startup Ideas and ValidationStartup Ideas and Validation
Startup Ideas and Validation
Yevgeniy Brikman
 
A Guide to Hiring for your Startup
A Guide to Hiring for your StartupA Guide to Hiring for your Startup
A Guide to Hiring for your Startup
Yevgeniy Brikman
 
Startup DNA: Speed Wins
Startup DNA: Speed WinsStartup DNA: Speed Wins
Startup DNA: Speed Wins
Yevgeniy Brikman
 
Node.js vs Play Framework
Node.js vs Play FrameworkNode.js vs Play Framework
Node.js vs Play Framework
Yevgeniy Brikman
 
Rapid prototyping
Rapid prototypingRapid prototyping
Rapid prototyping
Yevgeniy Brikman
 
Kings of Code Hack Battle
Kings of Code Hack BattleKings of Code Hack Battle
Kings of Code Hack Battle
Yevgeniy Brikman
 
Hackdays and [in]cubator
Hackdays and [in]cubatorHackdays and [in]cubator
Hackdays and [in]cubator
Yevgeniy Brikman
 
Startup DNA: the formula behind successful startups in Silicon Valley (update...
Startup DNA: the formula behind successful startups in Silicon Valley (update...Startup DNA: the formula behind successful startups in Silicon Valley (update...
Startup DNA: the formula behind successful startups in Silicon Valley (update...
Yevgeniy Brikman
 
Dust.js
Dust.jsDust.js
LinkedIn Overview
LinkedIn OverviewLinkedIn Overview
LinkedIn Overview
Yevgeniy Brikman
 

More from Yevgeniy Brikman (15)

Cloud adoption fails - 5 ways deployments go wrong and 5 solutions
Cloud adoption fails - 5 ways deployments go wrong and 5 solutionsCloud adoption fails - 5 ways deployments go wrong and 5 solutions
Cloud adoption fails - 5 ways deployments go wrong and 5 solutions
 
How to test infrastructure code: automated testing for Terraform, Kubernetes,...
How to test infrastructure code: automated testing for Terraform, Kubernetes,...How to test infrastructure code: automated testing for Terraform, Kubernetes,...
How to test infrastructure code: automated testing for Terraform, Kubernetes,...
 
Gruntwork Executive Summary
Gruntwork Executive SummaryGruntwork Executive Summary
Gruntwork Executive Summary
 
The Truth About Startups: What I wish someone had told me about entrepreneurs...
The Truth About Startups: What I wish someone had told me about entrepreneurs...The Truth About Startups: What I wish someone had told me about entrepreneurs...
The Truth About Startups: What I wish someone had told me about entrepreneurs...
 
Agility Requires Safety
Agility Requires SafetyAgility Requires Safety
Agility Requires Safety
 
Startup Ideas and Validation
Startup Ideas and ValidationStartup Ideas and Validation
Startup Ideas and Validation
 
A Guide to Hiring for your Startup
A Guide to Hiring for your StartupA Guide to Hiring for your Startup
A Guide to Hiring for your Startup
 
Startup DNA: Speed Wins
Startup DNA: Speed WinsStartup DNA: Speed Wins
Startup DNA: Speed Wins
 
Node.js vs Play Framework
Node.js vs Play FrameworkNode.js vs Play Framework
Node.js vs Play Framework
 
Rapid prototyping
Rapid prototypingRapid prototyping
Rapid prototyping
 
Kings of Code Hack Battle
Kings of Code Hack BattleKings of Code Hack Battle
Kings of Code Hack Battle
 
Hackdays and [in]cubator
Hackdays and [in]cubatorHackdays and [in]cubator
Hackdays and [in]cubator
 
Startup DNA: the formula behind successful startups in Silicon Valley (update...
Startup DNA: the formula behind successful startups in Silicon Valley (update...Startup DNA: the formula behind successful startups in Silicon Valley (update...
Startup DNA: the formula behind successful startups in Silicon Valley (update...
 
Dust.js
Dust.jsDust.js
Dust.js
 
LinkedIn Overview
LinkedIn OverviewLinkedIn Overview
LinkedIn Overview
 

Recently uploaded

AWS Certified Solutions Architect Associate (SAA-C03)
AWS Certified Solutions Architect Associate (SAA-C03)AWS Certified Solutions Architect Associate (SAA-C03)
AWS Certified Solutions Architect Associate (SAA-C03)
HarpalGohil4
 
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
DanBrown980551
 
Must Know Postgres Extension for DBA and Developer during Migration
Must Know Postgres Extension for DBA and Developer during MigrationMust Know Postgres Extension for DBA and Developer during Migration
Must Know Postgres Extension for DBA and Developer during Migration
Mydbops
 
Leveraging the Graph for Clinical Trials and Standards
Leveraging the Graph for Clinical Trials and StandardsLeveraging the Graph for Clinical Trials and Standards
Leveraging the Graph for Clinical Trials and Standards
Neo4j
 
"$10 thousand per minute of downtime: architecture, queues, streaming and fin...
"$10 thousand per minute of downtime: architecture, queues, streaming and fin..."$10 thousand per minute of downtime: architecture, queues, streaming and fin...
"$10 thousand per minute of downtime: architecture, queues, streaming and fin...
Fwdays
 
Harnessing the Power of NLP and Knowledge Graphs for Opioid Research
Harnessing the Power of NLP and Knowledge Graphs for Opioid ResearchHarnessing the Power of NLP and Knowledge Graphs for Opioid Research
Harnessing the Power of NLP and Knowledge Graphs for Opioid Research
Neo4j
 
Session 1 - Intro to Robotic Process Automation.pdf
Session 1 - Intro to Robotic Process Automation.pdfSession 1 - Intro to Robotic Process Automation.pdf
Session 1 - Intro to Robotic Process Automation.pdf
UiPathCommunity
 
QR Secure: A Hybrid Approach Using Machine Learning and Security Validation F...
QR Secure: A Hybrid Approach Using Machine Learning and Security Validation F...QR Secure: A Hybrid Approach Using Machine Learning and Security Validation F...
QR Secure: A Hybrid Approach Using Machine Learning and Security Validation F...
AlexanderRichford
 
A Deep Dive into ScyllaDB's Architecture
A Deep Dive into ScyllaDB's ArchitectureA Deep Dive into ScyllaDB's Architecture
A Deep Dive into ScyllaDB's Architecture
ScyllaDB
 
Dandelion Hashtable: beyond billion requests per second on a commodity server
Dandelion Hashtable: beyond billion requests per second on a commodity serverDandelion Hashtable: beyond billion requests per second on a commodity server
Dandelion Hashtable: beyond billion requests per second on a commodity server
Antonios Katsarakis
 
GraphRAG for LifeSciences Hands-On with the Clinical Knowledge Graph
GraphRAG for LifeSciences Hands-On with the Clinical Knowledge GraphGraphRAG for LifeSciences Hands-On with the Clinical Knowledge Graph
GraphRAG for LifeSciences Hands-On with the Clinical Knowledge Graph
Neo4j
 
"Choosing proper type of scaling", Olena Syrota
"Choosing proper type of scaling", Olena Syrota"Choosing proper type of scaling", Olena Syrota
"Choosing proper type of scaling", Olena Syrota
Fwdays
 
Christine's Supplier Sourcing Presentaion.pptx
Christine's Supplier Sourcing Presentaion.pptxChristine's Supplier Sourcing Presentaion.pptx
Christine's Supplier Sourcing Presentaion.pptx
christinelarrosa
 
"NATO Hackathon Winner: AI-Powered Drug Search", Taras Kloba
"NATO Hackathon Winner: AI-Powered Drug Search",  Taras Kloba"NATO Hackathon Winner: AI-Powered Drug Search",  Taras Kloba
"NATO Hackathon Winner: AI-Powered Drug Search", Taras Kloba
Fwdays
 
ScyllaDB Tablets: Rethinking Replication
ScyllaDB Tablets: Rethinking ReplicationScyllaDB Tablets: Rethinking Replication
ScyllaDB Tablets: Rethinking Replication
ScyllaDB
 
Demystifying Knowledge Management through Storytelling
Demystifying Knowledge Management through StorytellingDemystifying Knowledge Management through Storytelling
Demystifying Knowledge Management through Storytelling
Enterprise Knowledge
 
Discover the Unseen: Tailored Recommendation of Unwatched Content
Discover the Unseen: Tailored Recommendation of Unwatched ContentDiscover the Unseen: Tailored Recommendation of Unwatched Content
Discover the Unseen: Tailored Recommendation of Unwatched Content
ScyllaDB
 
What is an RPA CoE? Session 1 – CoE Vision
What is an RPA CoE?  Session 1 – CoE VisionWhat is an RPA CoE?  Session 1 – CoE Vision
What is an RPA CoE? Session 1 – CoE Vision
DianaGray10
 
Introducing BoxLang : A new JVM language for productivity and modularity!
Introducing BoxLang : A new JVM language for productivity and modularity!Introducing BoxLang : A new JVM language for productivity and modularity!
Introducing BoxLang : A new JVM language for productivity and modularity!
Ortus Solutions, Corp
 
Containers & AI - Beauty and the Beast!?!
Containers & AI - Beauty and the Beast!?!Containers & AI - Beauty and the Beast!?!
Containers & AI - Beauty and the Beast!?!
Tobias Schneck
 

Recently uploaded (20)

AWS Certified Solutions Architect Associate (SAA-C03)
AWS Certified Solutions Architect Associate (SAA-C03)AWS Certified Solutions Architect Associate (SAA-C03)
AWS Certified Solutions Architect Associate (SAA-C03)
 
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
 
Must Know Postgres Extension for DBA and Developer during Migration
Must Know Postgres Extension for DBA and Developer during MigrationMust Know Postgres Extension for DBA and Developer during Migration
Must Know Postgres Extension for DBA and Developer during Migration
 
Leveraging the Graph for Clinical Trials and Standards
Leveraging the Graph for Clinical Trials and StandardsLeveraging the Graph for Clinical Trials and Standards
Leveraging the Graph for Clinical Trials and Standards
 
"$10 thousand per minute of downtime: architecture, queues, streaming and fin...
"$10 thousand per minute of downtime: architecture, queues, streaming and fin..."$10 thousand per minute of downtime: architecture, queues, streaming and fin...
"$10 thousand per minute of downtime: architecture, queues, streaming and fin...
 
Harnessing the Power of NLP and Knowledge Graphs for Opioid Research
Harnessing the Power of NLP and Knowledge Graphs for Opioid ResearchHarnessing the Power of NLP and Knowledge Graphs for Opioid Research
Harnessing the Power of NLP and Knowledge Graphs for Opioid Research
 
Session 1 - Intro to Robotic Process Automation.pdf
Session 1 - Intro to Robotic Process Automation.pdfSession 1 - Intro to Robotic Process Automation.pdf
Session 1 - Intro to Robotic Process Automation.pdf
 
QR Secure: A Hybrid Approach Using Machine Learning and Security Validation F...
QR Secure: A Hybrid Approach Using Machine Learning and Security Validation F...QR Secure: A Hybrid Approach Using Machine Learning and Security Validation F...
QR Secure: A Hybrid Approach Using Machine Learning and Security Validation F...
 
A Deep Dive into ScyllaDB's Architecture
A Deep Dive into ScyllaDB's ArchitectureA Deep Dive into ScyllaDB's Architecture
A Deep Dive into ScyllaDB's Architecture
 
Dandelion Hashtable: beyond billion requests per second on a commodity server
Dandelion Hashtable: beyond billion requests per second on a commodity serverDandelion Hashtable: beyond billion requests per second on a commodity server
Dandelion Hashtable: beyond billion requests per second on a commodity server
 
GraphRAG for LifeSciences Hands-On with the Clinical Knowledge Graph
GraphRAG for LifeSciences Hands-On with the Clinical Knowledge GraphGraphRAG for LifeSciences Hands-On with the Clinical Knowledge Graph
GraphRAG for LifeSciences Hands-On with the Clinical Knowledge Graph
 
"Choosing proper type of scaling", Olena Syrota
"Choosing proper type of scaling", Olena Syrota"Choosing proper type of scaling", Olena Syrota
"Choosing proper type of scaling", Olena Syrota
 
Christine's Supplier Sourcing Presentaion.pptx
Christine's Supplier Sourcing Presentaion.pptxChristine's Supplier Sourcing Presentaion.pptx
Christine's Supplier Sourcing Presentaion.pptx
 
"NATO Hackathon Winner: AI-Powered Drug Search", Taras Kloba
"NATO Hackathon Winner: AI-Powered Drug Search",  Taras Kloba"NATO Hackathon Winner: AI-Powered Drug Search",  Taras Kloba
"NATO Hackathon Winner: AI-Powered Drug Search", Taras Kloba
 
ScyllaDB Tablets: Rethinking Replication
ScyllaDB Tablets: Rethinking ReplicationScyllaDB Tablets: Rethinking Replication
ScyllaDB Tablets: Rethinking Replication
 
Demystifying Knowledge Management through Storytelling
Demystifying Knowledge Management through StorytellingDemystifying Knowledge Management through Storytelling
Demystifying Knowledge Management through Storytelling
 
Discover the Unseen: Tailored Recommendation of Unwatched Content
Discover the Unseen: Tailored Recommendation of Unwatched ContentDiscover the Unseen: Tailored Recommendation of Unwatched Content
Discover the Unseen: Tailored Recommendation of Unwatched Content
 
What is an RPA CoE? Session 1 – CoE Vision
What is an RPA CoE?  Session 1 – CoE VisionWhat is an RPA CoE?  Session 1 – CoE Vision
What is an RPA CoE? Session 1 – CoE Vision
 
Introducing BoxLang : A new JVM language for productivity and modularity!
Introducing BoxLang : A new JVM language for productivity and modularity!Introducing BoxLang : A new JVM language for productivity and modularity!
Introducing BoxLang : A new JVM language for productivity and modularity!
 
Containers & AI - Beauty and the Beast!?!
Containers & AI - Beauty and the Beast!?!Containers & AI - Beauty and the Beast!?!
Containers & AI - Beauty and the Beast!?!
 

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
  • 32.
  • 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
  • 80.
  • 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