SlideShare a Scribd company logo
1 of 163
Download to read offline
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

PoP - “Platform of Platforms”: Framework for building Single-Page Application...
PoP - “Platform of Platforms”: Framework for building Single-Page Application...PoP - “Platform of Platforms”: Framework for building Single-Page Application...
PoP - “Platform of Platforms”: Framework for building Single-Page Application...Leonardo Losoviz
 
Spring Boot and REST API
Spring Boot and REST APISpring Boot and REST API
Spring Boot and REST API07.pallav
 
Gradle - the Enterprise Automation Tool
Gradle  - the Enterprise Automation ToolGradle  - the Enterprise Automation Tool
Gradle - the Enterprise Automation ToolIzzet Mustafaiev
 
Introduction to Spring Framework
Introduction to Spring FrameworkIntroduction to Spring Framework
Introduction to Spring Framework Serhat Can
 
REST APIs with Spring
REST APIs with SpringREST APIs with Spring
REST APIs with SpringJoshua Long
 
스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해beom kyun choi
 
Introduction to AngularJS
Introduction to AngularJSIntroduction to AngularJS
Introduction to AngularJSDavid Parsons
 
Vim 讓你寫 Ruby 的速度飛起來
Vim 讓你寫 Ruby 的速度飛起來Vim 讓你寫 Ruby 的速度飛起來
Vim 讓你寫 Ruby 的速度飛起來Chris Houng
 
Unit testing JavaScript: Jasmine & karma intro
Unit testing JavaScript: Jasmine & karma introUnit testing JavaScript: Jasmine & karma intro
Unit testing JavaScript: Jasmine & karma introMaurice De Beijer [MVP]
 
Rest API Automation with REST Assured
Rest API Automation with REST AssuredRest API Automation with REST Assured
Rest API Automation with REST AssuredTO THE NEW Pvt. Ltd.
 
Spring boot - an introduction
Spring boot - an introductionSpring boot - an introduction
Spring boot - an introductionJonathan Holloway
 
Writing and using Hamcrest Matchers
Writing and using Hamcrest MatchersWriting and using Hamcrest Matchers
Writing and using Hamcrest MatchersShai Yallin
 
Android & iOS Automation Using Appium
Android & iOS Automation Using AppiumAndroid & iOS Automation Using Appium
Android & iOS Automation Using AppiumMindfire Solutions
 

What's hot (20)

PoP - “Platform of Platforms”: Framework for building Single-Page Application...
PoP - “Platform of Platforms”: Framework for building Single-Page Application...PoP - “Platform of Platforms”: Framework for building Single-Page Application...
PoP - “Platform of Platforms”: Framework for building Single-Page Application...
 
Spring Boot and REST API
Spring Boot and REST APISpring Boot and REST API
Spring Boot and REST API
 
Spring Boot
Spring BootSpring Boot
Spring Boot
 
WebLogic FAQs
WebLogic FAQsWebLogic FAQs
WebLogic FAQs
 
Gradle - the Enterprise Automation Tool
Gradle  - the Enterprise Automation ToolGradle  - the Enterprise Automation Tool
Gradle - the Enterprise Automation Tool
 
Spring Boot
Spring BootSpring Boot
Spring Boot
 
Introduction to Spring Framework
Introduction to Spring FrameworkIntroduction to Spring Framework
Introduction to Spring Framework
 
REST APIs with Spring
REST APIs with SpringREST APIs with Spring
REST APIs with Spring
 
스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해
 
Introduction to AngularJS
Introduction to AngularJSIntroduction to AngularJS
Introduction to AngularJS
 
15. DateTime API.ppt
15. DateTime API.ppt15. DateTime API.ppt
15. DateTime API.ppt
 
Vim 讓你寫 Ruby 的速度飛起來
Vim 讓你寫 Ruby 的速度飛起來Vim 讓你寫 Ruby 的速度飛起來
Vim 讓你寫 Ruby 的速度飛起來
 
Unit testing JavaScript: Jasmine & karma intro
Unit testing JavaScript: Jasmine & karma introUnit testing JavaScript: Jasmine & karma intro
Unit testing JavaScript: Jasmine & karma intro
 
Core java
Core javaCore java
Core java
 
Rest API Automation with REST Assured
Rest API Automation with REST AssuredRest API Automation with REST Assured
Rest API Automation with REST Assured
 
API Testing
API TestingAPI Testing
API Testing
 
Spring boot - an introduction
Spring boot - an introductionSpring boot - an introduction
Spring boot - an introduction
 
Spring MVC
Spring MVCSpring MVC
Spring MVC
 
Writing and using Hamcrest Matchers
Writing and using Hamcrest MatchersWriting and using Hamcrest Matchers
Writing and using Hamcrest Matchers
 
Android & iOS Automation Using Appium
Android & iOS Automation Using AppiumAndroid & iOS Automation Using Appium
Android & iOS Automation Using Appium
 

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 ScalaYevgeniy 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
 
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 ECSYevgeniy 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 scalaQuentin 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 MLPaco 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 WorkflowsPaco 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 - 4DevelopersSam Brannen
 
Reactive Programming With Akka - Lessons Learned
Reactive Programming With Akka - Lessons LearnedReactive Programming With Akka - Lessons Learned
Reactive Programming With Akka - Lessons LearnedDaniel 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 FrameworkAdam Warski
 
A Sceptical Guide to Functional Programming
A Sceptical Guide to Functional ProgrammingA Sceptical Guide to Functional Programming
A Sceptical Guide to Functional ProgrammingGarth Gilmour
 
Actor Based Asyncronous IO in Akka
Actor Based Asyncronous IO in AkkaActor Based Asyncronous IO in Akka
Actor Based Asyncronous IO in Akkadrewhk
 
Effective akka scalaio
Effective akka scalaioEffective akka scalaio
Effective akka scalaioshinolajla
 
DUMP-2015: «Распределенная обработка миллионов документов на Scala и Akka» Ст...
DUMP-2015: «Распределенная обработка миллионов документов на Scala и Akka» Ст...DUMP-2015: «Распределенная обработка миллионов документов на Scala и Akka» Ст...
DUMP-2015: «Распределенная обработка миллионов документов на Scala и Akka» Ст...it-people
 
Play Template Engine Based On Scala
Play Template Engine Based On ScalaPlay Template Engine Based On Scala
Play Template Engine Based On ScalaKnoldus Inc.
 

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!
 
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
 
Play Template Engine Based On Scala
Play Template Engine Based On ScalaPlay Template Engine Based On Scala
Play Template Engine Based On Scala
 

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 SymfonyIgnacio Martín
 
Angular resolver tutorial
Angular resolver tutorialAngular resolver tutorial
Angular resolver tutorialKaty 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 PickinGavin 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 productsScaleway
 
"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 pptkamal 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'sAntô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 2019Joe 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'sOrtus 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 overviewMaxim 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 applicationsEvangelia 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 JavaOne2014hezamu
 
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 architectureSomenath Mukhopadhyay
 
Http programming in play
Http programming in playHttp programming in play
Http programming in playKnoldus 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 desaijinaldesailive
 

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 solutionsYevgeniy 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
 
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 codeYevgeniy Brikman
 
Gruntwork Executive Summary
Gruntwork Executive SummaryGruntwork Executive Summary
Gruntwork Executive SummaryYevgeniy Brikman
 
Reusable, composable, battle-tested Terraform modules
Reusable, composable, battle-tested Terraform modulesReusable, composable, battle-tested Terraform modules
Reusable, composable, battle-tested Terraform modulesYevgeniy 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
 
Comprehensive Terraform Training
Comprehensive Terraform TrainingComprehensive Terraform Training
Comprehensive Terraform TrainingYevgeniy Brikman
 
Startup Ideas and Validation
Startup Ideas and ValidationStartup Ideas and Validation
Startup Ideas and ValidationYevgeniy 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 StartupYevgeniy 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
 

More from Yevgeniy Brikman (18)

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,...
 
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
 
Gruntwork Executive Summary
Gruntwork Executive SummaryGruntwork Executive Summary
Gruntwork Executive Summary
 
Reusable, composable, battle-tested Terraform modules
Reusable, composable, battle-tested Terraform modulesReusable, composable, battle-tested Terraform modules
Reusable, composable, battle-tested Terraform modules
 
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...
 
Comprehensive Terraform Training
Comprehensive Terraform TrainingComprehensive Terraform Training
Comprehensive Terraform Training
 
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

4. Cobus Valentine- Cybersecurity Threats and Solutions for the Public Sector
4. Cobus Valentine- Cybersecurity Threats and Solutions for the Public Sector4. Cobus Valentine- Cybersecurity Threats and Solutions for the Public Sector
4. Cobus Valentine- Cybersecurity Threats and Solutions for the Public Sectoritnewsafrica
 
Glenn Lazarus- Why Your Observability Strategy Needs Security Observability
Glenn Lazarus- Why Your Observability Strategy Needs Security ObservabilityGlenn Lazarus- Why Your Observability Strategy Needs Security Observability
Glenn Lazarus- Why Your Observability Strategy Needs Security Observabilityitnewsafrica
 
Microservices, Docker deploy and Microservices source code in C#
Microservices, Docker deploy and Microservices source code in C#Microservices, Docker deploy and Microservices source code in C#
Microservices, Docker deploy and Microservices source code in C#Karmanjay Verma
 
Top 10 Hubspot Development Companies in 2024
Top 10 Hubspot Development Companies in 2024Top 10 Hubspot Development Companies in 2024
Top 10 Hubspot Development Companies in 2024TopCSSGallery
 
Tampa BSides - The No BS SOC (slides from April 6, 2024 talk)
Tampa BSides - The No BS SOC (slides from April 6, 2024 talk)Tampa BSides - The No BS SOC (slides from April 6, 2024 talk)
Tampa BSides - The No BS SOC (slides from April 6, 2024 talk)Mark Simos
 
Digital Tools & AI in Career Development
Digital Tools & AI in Career DevelopmentDigital Tools & AI in Career Development
Digital Tools & AI in Career DevelopmentMahmoud Rabie
 
Emixa Mendix Meetup 11 April 2024 about Mendix Native development
Emixa Mendix Meetup 11 April 2024 about Mendix Native developmentEmixa Mendix Meetup 11 April 2024 about Mendix Native development
Emixa Mendix Meetup 11 April 2024 about Mendix Native developmentPim van der Noll
 
A Journey Into the Emotions of Software Developers
A Journey Into the Emotions of Software DevelopersA Journey Into the Emotions of Software Developers
A Journey Into the Emotions of Software DevelopersNicole Novielli
 
Why device, WIFI, and ISP insights are crucial to supporting remote Microsoft...
Why device, WIFI, and ISP insights are crucial to supporting remote Microsoft...Why device, WIFI, and ISP insights are crucial to supporting remote Microsoft...
Why device, WIFI, and ISP insights are crucial to supporting remote Microsoft...panagenda
 
A Glance At The Java Performance Toolbox
A Glance At The Java Performance ToolboxA Glance At The Java Performance Toolbox
A Glance At The Java Performance ToolboxAna-Maria Mihalceanu
 
UiPath Community: Communication Mining from Zero to Hero
UiPath Community: Communication Mining from Zero to HeroUiPath Community: Communication Mining from Zero to Hero
UiPath Community: Communication Mining from Zero to HeroUiPathCommunity
 
So einfach geht modernes Roaming fuer Notes und Nomad.pdf
So einfach geht modernes Roaming fuer Notes und Nomad.pdfSo einfach geht modernes Roaming fuer Notes und Nomad.pdf
So einfach geht modernes Roaming fuer Notes und Nomad.pdfpanagenda
 
Kuma Meshes Part I - The basics - A tutorial
Kuma Meshes Part I - The basics - A tutorialKuma Meshes Part I - The basics - A tutorial
Kuma Meshes Part I - The basics - A tutorialJoão Esperancinha
 
Potential of AI (Generative AI) in Business: Learnings and Insights
Potential of AI (Generative AI) in Business: Learnings and InsightsPotential of AI (Generative AI) in Business: Learnings and Insights
Potential of AI (Generative AI) in Business: Learnings and InsightsRavi Sanghani
 
Modern Roaming for Notes and Nomad – Cheaper Faster Better Stronger
Modern Roaming for Notes and Nomad – Cheaper Faster Better StrongerModern Roaming for Notes and Nomad – Cheaper Faster Better Stronger
Modern Roaming for Notes and Nomad – Cheaper Faster Better Strongerpanagenda
 
Design pattern talk by Kaya Weers - 2024 (v2)
Design pattern talk by Kaya Weers - 2024 (v2)Design pattern talk by Kaya Weers - 2024 (v2)
Design pattern talk by Kaya Weers - 2024 (v2)Kaya Weers
 
Testing tools and AI - ideas what to try with some tool examples
Testing tools and AI - ideas what to try with some tool examplesTesting tools and AI - ideas what to try with some tool examples
Testing tools and AI - ideas what to try with some tool examplesKari Kakkonen
 
Arizona Broadband Policy Past, Present, and Future Presentation 3/25/24
Arizona Broadband Policy Past, Present, and Future Presentation 3/25/24Arizona Broadband Policy Past, Present, and Future Presentation 3/25/24
Arizona Broadband Policy Past, Present, and Future Presentation 3/25/24Mark Goldstein
 
Microsoft 365 Copilot: How to boost your productivity with AI – Part one: Ado...
Microsoft 365 Copilot: How to boost your productivity with AI – Part one: Ado...Microsoft 365 Copilot: How to boost your productivity with AI – Part one: Ado...
Microsoft 365 Copilot: How to boost your productivity with AI – Part one: Ado...Nikki Chapple
 
Generative AI - Gitex v1Generative AI - Gitex v1.pptx
Generative AI - Gitex v1Generative AI - Gitex v1.pptxGenerative AI - Gitex v1Generative AI - Gitex v1.pptx
Generative AI - Gitex v1Generative AI - Gitex v1.pptxfnnc6jmgwh
 

Recently uploaded (20)

4. Cobus Valentine- Cybersecurity Threats and Solutions for the Public Sector
4. Cobus Valentine- Cybersecurity Threats and Solutions for the Public Sector4. Cobus Valentine- Cybersecurity Threats and Solutions for the Public Sector
4. Cobus Valentine- Cybersecurity Threats and Solutions for the Public Sector
 
Glenn Lazarus- Why Your Observability Strategy Needs Security Observability
Glenn Lazarus- Why Your Observability Strategy Needs Security ObservabilityGlenn Lazarus- Why Your Observability Strategy Needs Security Observability
Glenn Lazarus- Why Your Observability Strategy Needs Security Observability
 
Microservices, Docker deploy and Microservices source code in C#
Microservices, Docker deploy and Microservices source code in C#Microservices, Docker deploy and Microservices source code in C#
Microservices, Docker deploy and Microservices source code in C#
 
Top 10 Hubspot Development Companies in 2024
Top 10 Hubspot Development Companies in 2024Top 10 Hubspot Development Companies in 2024
Top 10 Hubspot Development Companies in 2024
 
Tampa BSides - The No BS SOC (slides from April 6, 2024 talk)
Tampa BSides - The No BS SOC (slides from April 6, 2024 talk)Tampa BSides - The No BS SOC (slides from April 6, 2024 talk)
Tampa BSides - The No BS SOC (slides from April 6, 2024 talk)
 
Digital Tools & AI in Career Development
Digital Tools & AI in Career DevelopmentDigital Tools & AI in Career Development
Digital Tools & AI in Career Development
 
Emixa Mendix Meetup 11 April 2024 about Mendix Native development
Emixa Mendix Meetup 11 April 2024 about Mendix Native developmentEmixa Mendix Meetup 11 April 2024 about Mendix Native development
Emixa Mendix Meetup 11 April 2024 about Mendix Native development
 
A Journey Into the Emotions of Software Developers
A Journey Into the Emotions of Software DevelopersA Journey Into the Emotions of Software Developers
A Journey Into the Emotions of Software Developers
 
Why device, WIFI, and ISP insights are crucial to supporting remote Microsoft...
Why device, WIFI, and ISP insights are crucial to supporting remote Microsoft...Why device, WIFI, and ISP insights are crucial to supporting remote Microsoft...
Why device, WIFI, and ISP insights are crucial to supporting remote Microsoft...
 
A Glance At The Java Performance Toolbox
A Glance At The Java Performance ToolboxA Glance At The Java Performance Toolbox
A Glance At The Java Performance Toolbox
 
UiPath Community: Communication Mining from Zero to Hero
UiPath Community: Communication Mining from Zero to HeroUiPath Community: Communication Mining from Zero to Hero
UiPath Community: Communication Mining from Zero to Hero
 
So einfach geht modernes Roaming fuer Notes und Nomad.pdf
So einfach geht modernes Roaming fuer Notes und Nomad.pdfSo einfach geht modernes Roaming fuer Notes und Nomad.pdf
So einfach geht modernes Roaming fuer Notes und Nomad.pdf
 
Kuma Meshes Part I - The basics - A tutorial
Kuma Meshes Part I - The basics - A tutorialKuma Meshes Part I - The basics - A tutorial
Kuma Meshes Part I - The basics - A tutorial
 
Potential of AI (Generative AI) in Business: Learnings and Insights
Potential of AI (Generative AI) in Business: Learnings and InsightsPotential of AI (Generative AI) in Business: Learnings and Insights
Potential of AI (Generative AI) in Business: Learnings and Insights
 
Modern Roaming for Notes and Nomad – Cheaper Faster Better Stronger
Modern Roaming for Notes and Nomad – Cheaper Faster Better StrongerModern Roaming for Notes and Nomad – Cheaper Faster Better Stronger
Modern Roaming for Notes and Nomad – Cheaper Faster Better Stronger
 
Design pattern talk by Kaya Weers - 2024 (v2)
Design pattern talk by Kaya Weers - 2024 (v2)Design pattern talk by Kaya Weers - 2024 (v2)
Design pattern talk by Kaya Weers - 2024 (v2)
 
Testing tools and AI - ideas what to try with some tool examples
Testing tools and AI - ideas what to try with some tool examplesTesting tools and AI - ideas what to try with some tool examples
Testing tools and AI - ideas what to try with some tool examples
 
Arizona Broadband Policy Past, Present, and Future Presentation 3/25/24
Arizona Broadband Policy Past, Present, and Future Presentation 3/25/24Arizona Broadband Policy Past, Present, and Future Presentation 3/25/24
Arizona Broadband Policy Past, Present, and Future Presentation 3/25/24
 
Microsoft 365 Copilot: How to boost your productivity with AI – Part one: Ado...
Microsoft 365 Copilot: How to boost your productivity with AI – Part one: Ado...Microsoft 365 Copilot: How to boost your productivity with AI – Part one: Ado...
Microsoft 365 Copilot: How to boost your productivity with AI – Part one: Ado...
 
Generative AI - Gitex v1Generative AI - Gitex v1.pptx
Generative AI - Gitex v1Generative AI - Gitex v1.pptxGenerative AI - Gitex v1Generative AI - Gitex v1.pptx
Generative AI - Gitex v1Generative AI - Gitex v1.pptx
 

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