Permalink
Browse files

Added `staticCache()` middleware

this cache layer is ~29% faster than node-static
plus we have more features :) so there is very
little reason to it now
  • Loading branch information...
1 parent 91edd0b commit 1efe3aa339b2a6d217bdd333daec19c11d6c1cba @tj tj committed Aug 31, 2011
Showing with 253 additions and 0 deletions.
  1. +31 −0 Readme.md
  2. +81 −0 lib/cache.js
  3. +1 −0 lib/index.js
  4. +1 −0 lib/middleware/static.js
  5. +110 −0 lib/middleware/staticCache.js
  6. +17 −0 lib/utils.js
  7. +12 −0 test/utils.test.js
View
@@ -1,10 +1,41 @@
+
# Connect
Connect is an extensible HTTP server framework for [node](http://nodejs.org), providing high performance "plugins" known as _middleware_.
Connect is bundled with over _14_ commonly used middleware, including
a logger, session support, cookie parser, and [more](http://senchalabs.github.com/connect). Be sure to view the 1.0 [documentation](http://senchalabs.github.com/connect/).
+## Middleware
+
+ - csrf
+ - basicAuth
+ - bodyParser
+ - cookieParser
+ - directory
+ - errorHandler
+ - favicon
+ - limit
+ - logger
+ - methodOverride
+ - query
+ - responsetime
+ - session
+ - static
+ - staticCache
+ - vhost
+
+## Static file serving
+
+ The benchmarks below show the `static()` middleware
+ requests per second vs `static()` with the `staticCache()`
+ cache layer, out performing other popular node modules,
+ while maintaining more features like Range request etc.
+
+ - static(): 2700 rps
+ - node-static: 5300 rps
+ - static() + staticCache(): 7500 rps
+
## Running Tests
first:
View
@@ -0,0 +1,81 @@
+
+/*!
+ * Connect - Cache
+ * Copyright(c) 2011 Sencha Inc.
+ * MIT Licensed
+ */
+
+/**
+ * Expose `Cache`.
+ */
+
+module.exports = Cache;
+
+/**
+ * LRU cache store.
+ *
+ * @param {Number} limit
+ * @api private
+ */
+
+function Cache(limit) {
+ this.store = {};
+ this.keys = [];
+ this.limit = limit;
+}
+
+/**
+ * Touch `key`, promoting the object.
+ *
+ * @param {String} key
+ * @param {Number} i
+ * @api private
+ */
+
+Cache.prototype.touch = function(key, i){
+ this.keys.splice(i,1);
+ this.keys.push(key);
+};
+
+/**
+ * Remove `key`.
+ *
+ * @param {String} key
+ * @api private
+ */
+
+Cache.prototype.remove = function(key){
+ delete this.store[key];
+};
+
+/**
+ * Get the object stored for `key`.
+ *
+ * @param {String} key
+ * @return {Array}
+ * @api private
+ */
+
+Cache.prototype.get = function(key){
+ return this.store[key];
+};
+
+/**
+ * Add a cache `key`.
+ *
+ * @param {String} key
+ * @return {Array}
+ * @api private
+ */
+
+Cache.prototype.add = function(key){
+ // initialize store
+ var len = this.keys.push(key);
+
+ // limit reached, invalid LRU
+ if (len > this.limit) this.remove(this.keys.shift());
+
+ var arr = this.store[key] = [];
+ arr.createdAt = new Date;
+ return arr;
+};
View
@@ -28,6 +28,7 @@
* - [methodOverride](middleware-methodOverride.html) faux HTTP method support
* - [responseTime](middleware-responseTime.html) calculates response-time and exposes via X-Response-Time
* - [router](middleware-router.html) provides rich Sinatra / Express-like routing
+ * - [staticCache](middleware-staticCache.html) memory cache layer for the static() middleware
* - [static](middleware-static.html) streaming static file server supporting `Range` and more
* - [directory](middleware-directory.html) directory listing middleware
* - [vhost](middleware-vhost.html) virtual host sub-domain mapping middleware
View
@@ -206,6 +206,7 @@ var send = exports.send = function(req, res, next, options){
// stream
var stream = fs.createReadStream(path, opts);
+ req.emit('static', stream);
stream.pipe(res);
// callback
@@ -0,0 +1,110 @@
+
+/*!
+ * Connect - staticCache
+ * Copyright(c) 2011 Sencha Inc.
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var http = require('http')
+ , Cache = require('../cache')
+ , url = require('url')
+ , fs = require('fs');
+
+/**
+ * Enables a memory cache layer on top of
+ * the `static()` middleware, serving popular
+ * static files.
+ *
+ * By default a maximum of 128 objects are
+ * held in cache, with a max of 256k each,
+ * totalling ~32mb.
+ *
+ * A Least-Recently-Used (LRU) cache algo
+ * is implemented through the `Cache` object,
+ * simply rotating cache objects as they are
+ * hit. This means that increasingly popular
+ * objects maintain their positions while
+ * others get shoved out of the stack and
+ * garbage collected.
+ *
+ * Benchmarks:
+ *
+ * static(): 2700 rps
+ * node-static: 5300 rps
+ * static() + staticCache(): 7500 rps
+ *
+ * Options:
+ *
+ * - `maxObjects` max cache objects [128]
+ * - `maxLength` max cache object length 256kb
+ *
+ * @param {Type} name
+ * @return {Type}
+ * @api public
+ */
+
+module.exports = function staticCache(options){
+ var options = options || {}
+ , cache = new Cache(options.maxObjects || 128)
+ , maxlen = options.maxLength || 1024 * 256;
+
+ return function staticCache(req, res, next){
+ var path = url.parse(req.url).pathname
+ , hit = cache.get(path)
+ , cc = req.headers['cache-control']
+ , header;
+
+ // cache hit
+ if (hit && hit.complete) {
+ // TODO: finish meeee... http caching... throttling etc
+ header = hit[0];
+ header.Age = (new Date - hit.createdAt) / 1000 | 0;
+
+ // HEAD support
+ if ('HEAD' == req.method) {
+ header['content-length'] = 0;
+ res.writeHead(200, header);
+ return res.end();
+ }
+
+ // respond with cache
+ res.writeHead(200, header);
+ for (var i = 1, len = hit.length; i < len; ++i) {
+ res.write(hit[i]);
+ }
+ res.end();
+ return;
+ }
+
+ // cache static
+ req.on('static', function(stream){
+ // ignore larger files
+ var contentLength = res._headers['content-length'];
+ if (!contentLength || contentLength > maxlen) return;
+
+ // exists
+ if (cache.get(path)) return;
+
+ // add the cache object
+ var arr = cache.add(path);
+ arr.push(res._headers);
+
+ // store the chunks
+ stream.on('data', function(chunk){
+ arr.push(chunk);
+ });
+
+ // flag it as complete
+ stream.on('end', function(){
+ arr.complete = true;
+ });
+ });
+
+ next();
+ }
+};
+
View
@@ -394,6 +394,23 @@ exports.parseRange = function(size, str){
return valid ? arr : undefined;
};
+/**
+ * Parse the given Cache-Control `str`.
+ *
+ * @param {String} str
+ * @return {Object}
+ * @api public
+ */
+
+exports.parseCacheControl = function(str){
+ var parts = str.split('=')
+ , key = parts.shift()
+ , val = parseInt(parts.shift(), 10)
+ , obj = {};
+ obj[key] = isNaN(val) ? true : val;
+ return obj;
+};
+
/**
* Convert array-like object to an `Array`.
*
View
@@ -19,6 +19,18 @@ module.exports = {
}
},
+ 'test utils.parseCacheControl()': function(){
+ var parse = utils.parseCacheControl;
+ parse('no-cache').should.eql({ 'no-cache': true });
+ parse('no-store').should.eql({ 'no-store': true });
+ parse('no-transform').should.eql({ 'no-transform': true });
+ parse('only-if-cached').should.eql({ 'only-if-cached': true });
+ parse('max-age=0').should.eql({ 'max-age': 0 });
+ parse('max-age=60').should.eql({ 'max-age': 60 });
+ parse('max-stale=60').should.eql({ 'max-stale': 60 });
+ parse('min-fresh=60').should.eql({ 'min-fresh': 60 });
+ },
+
'test parseCookie()': function(){
utils.parseCookie('foo=bar').should.eql({ foo: 'bar' });
utils.parseCookie('SID=123').should.eql({ sid: '123' });

0 comments on commit 1efe3aa

Please sign in to comment.