Sunday, July 31, 2011

Stupid SPDY Tricks

‹prev | My Chain | next›

For my last official post of the SPDY chain (*sniff*), I would like to goof around with SPDY server push. I am fairly convinced that SPDY server push, pushing data directly into browser cache, is the killer SPDY feature. I am so enamored of it, that I dedicated two chapters to in in the SPDY Book.

One of the many interesting aspects of SPDY push is its ability to defer the push until the server is ready to send it along. What this means in practice is that the server can jam out a bunch of static data to the browser almost immediately. It can then send out dynamic information once it has had a chance to perform any necessary computation.

To see this in action, I propose to send out a static page with very little on it. I will then push out a bunch of static data (jQuery, CSS, images) and a bit of dynamic data.

First, I create a regular express.js application:
➜  samples git:(master) ✗ express goofy-spdy
create : goofy-spdy
create : goofy-spdy/package.json
create : goofy-spdy/app.js
create : goofy-spdy/views
create : goofy-spdy/views/layout.jade
create : goofy-spdy/views/index.jade
create : goofy-spdy/public/stylesheets
create : goofy-spdy/public/stylesheets/style.css
create : goofy-spdy/public/images
create : goofy-spdy/public/javascripts
create : goofy-spdy/logs
create : goofy-spdy/pids
I then install the spdy, express-spdy, and jade npm packages. The spdy package is a dependency of express-spdy, so it is already installed. I have to explicitly install it so that I can get access to the createPushStream() method in the application (ah, the joys of npm).

In the app.js file, I configure my server be SPDY-ized and to perform push directly into the browser cache:
var express = require('express-spdy')
, fs = require('fs')
, createPushStream = require('spdy').createPushStream
, host = "https://jaynestown.local:3000/";


var app = module.exports = express.createServer({
key: fs.readFileSync(__dirname + '/keys/jaynestown.key'),
cert: fs.readFileSync(__dirname + '/keys/jaynestown.crt'),
ca: fs.readFileSync(__dirname + '/keys/jaynestown.csr'),
NPNProtocols: ['spdy/2'],
push: awesome_push
});
Before taking a look at that awesome_push callback, let's first see the home page of this site:



Each letter in "Hello" is a separate image and the page also has some CSS and pulls in jQuery:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to this Awesome Site!</title>
<link rel="stylesheet" href="/stylesheets/style.css"/>
<script src="/javascripts/jquery-1.6.2.min.js"></script>
</head>
<body>

<h1>Just wanna say...</h1>

<div id="hello">
<img src="/images/00-h.jpg">
<img src="/images/01-e.jpg">
<img src="/images/02-l.jpg">
<img src="/images/03-l.jpg">
<img src="/images/04-o.jpg">
</div>

</body>
</html>
To make the page a bit more dynamic, I add the following "profile" section to the HTML:
    <div id="profile" style="display:none">
<p>
Welcome back, <span id="name"></span>.
</p>
<p>
Today is <span id="date"></span>.
</p>
<p>
Your favorite color is: <span id="favorite_color"></span>.
</p>
<p>
I worked really hard to come up with this.
I think your favorite number might be
<span id="random_number"></span>.
</p>
</div>
That profile information will come from Javascript that will be injected into the browser cache:
  <script src="profile.js"></script>
<script language="javascript">
$(function() {
if (profile.name) {
$("#profile").show();
$("#name").text(profile.name);
$("#date").text(profile.date);
$("#favorite_color").text(profile.favorite_color);
$("#random_number").text(profile.random_number);
}
});
</script>
On the file system, profile.js will be empty:
➜  goofy-spdy git:(master) ✗ cat public/profile.js 
var profile = {};
But using dynamic SPDY server push, we can populate the profile Javascript object with something interesting.

So back to the express-spdy backend. The awesome_push() callback will contain:
function awesome_push(pusher) {
// Only push in response to the first request
if (pusher.streamID > 1) return;

// Oh boy, this is going to take a while to compute...
long_running_push(pusher);

// Push resources that can be deferred until after the response is
// sent
pusher.pushLater([
local_path_and_url("stylesheets/style.css"),
local_path_and_url("javascripts/jquery-1.6.2.min.js"),
local_path_and_url("images/00-h.jpg"),
local_path_and_url("images/01-e.jpg"),
local_path_and_url("images/02-l.jpg"),
local_path_and_url("images/03-l.jpg"),
local_path_and_url("images/04-o.jpg")
]);
}
First up, we start a long running push operation with a call to long_running_push(). This node.js, so it will not block. Then we push the stylesheet, JS, and images directly into the browser cache. This is a "later" push in that the data will be sent after the original HTML response is complete.

Finally, in the long_running_push() function, push out the data—once it is available:
function long_running_push(pusher) {
var url = host + "profile.js"
, push_stream = createPushStream(pusher.cframe, pusher.c, url);

// Send the push stream headers
// Ew. Need to expose an API for this...
push_stream._flushHead();
push_stream._written = true;

setTimeout(function () {
// Write the push stream data
push_stream.write(
'var profile = {' +
' name: "Bob",' +
' favorite_color: "yellow",' +
' date: "' + (new Date).toString() + '",' +
' random_number: "' + Math.ceil(Math.random() * 10) + '"' +
'}'
);
push_stream.end();
}, 3*1000);
};
Loading up the app in Chrome, I see the same homepage—for 3 seconds. After 3 seconds have elapsed, and the computationally intensive long_running_push finally sends back its data, I see this in the browser:



Granted, it took a long time for my poor little laptop to calculate my favorite number, but that's not the point.

The point is that the user experience was excellent. The entire page rendered in under a second because the browser had to make only one request to get everything. There was no round trip to request the various Javascript, CSS, and image files. A single request resulted in everything being jammed into browser cache via SPDY server push.

Even more exciting is that dynamic data was injected in the browser cache and (this is important thing) it did so without blocking any requests. Think about that. In vanilla HTTP, a computationally intensive request can block all requests on an interweb tube. Since browsers only get 6 interweb tubes to each site, a few of these types of requests and the page grinds to a halt.

With SPDY server push, nothing blocked. Everything flew across the wire when it was ready—no waiting on round trip times, blocking resources or anything. Just raw speed.

Awesome sauce. Wanna learn more? Buy my SPDY Book!


Day #99

2 comments:

  1. Is there a race condition in this example where the client receives and parses the html containing script src="profile.js" and requests it from the server while the server is already busy trying to generate it? Does this introduce any problems or inefficiencies, like returning a script for profile.js twice if the client happens to parse and request profile.js before it receives the server-pushed version?

    ReplyDelete
    Replies
    1. Nope. No race condition here :)

      The first thing the long running SPDY push does is write the headers of the thing being pushed (the call to the _flushHead() private method). This goes out after the SYN_REPLY for the page, but before the DATA for the page. Thus, the browser already knows that "profile.js" is being sent to its cache before it has seen one byte of the web page that references it. By the time the browser parses the web page, noticing that it needs "profile.js", it has already determined that "profile.js" is on its way and won't re-request.

      If the push waited until after the DATA had been sent, then I'd have race condition on my hands. But, per the spec, "the SYN_STREAM for the pushed resources must be sent prior to sending any content which could allow the client to discover the pushed resource and request it." So it's all good :)

      Delete