Tuesday, November 13, 2012

Benchmarking my Dart HTTP Server

‹prev | My Chain | next›

I did a bad thing. Yesterday, I optimized my Dart-based web server. The problem, of course, is that I optimized without benchmarking. Shame on me.

To rectify my mistake, I will used the Apache Bench tool to measure the performance of my two approaches for serving up static files in my public directory.

Approach #1 used the same publicPath() function in both the route matcher function and the route response function:
  app.addRequestHandler(
    (req) {
      if (req.method != 'GET') return false;

      String path = publicPath(req.path);
      if (path == null) return false;

      return true;
    },
    (req, res) {
      var file = new File(publicPath(req.path));
      var stream = file.openInputStream();
      stream.pipe(res.outputStream);
    }
  );
Since publicPath() relies on a synchronous file-exists check, I postulated that making two blocking calls for the same request would slow things down.

Enter approach #2. In this approach, I store the path information in the session to be accessed by the route response function:
  app.addRequestHandler(
    (req) {
      if (req.method != 'GET') return false;

      String path = publicPath(req.path);
      if (path == null) return false;

      req.session().data = {'path': path};
      return true;
    },
    (req, res) {
      var file = new File(req.session().data['path']);
      var stream = file.openInputStream();
      stream.pipe(res.outputStream);
    }
  );
Ideally, I would have stuck the path information from the route matcher function into request headers. Unfortunately, Dart does not allow this as the headers are immutable. Hence this solution. But is this faster? Perhaps the expense of creating a session makes this more trouble than it is worth.

The results of ab for the duplicate sync-exists check in approach #1:
cstrom@londo:~/repos/dart-comics$ ab -n 10000 -c 2 http://localhost:8000/scripts/web/main.dart
....
Requests per second:    1555.80 [#/sec] (mean)
Time per request:       1.286 [ms] (mean)
And the results for my session-optimized approach #2:
cstrom@londo:~/repos/dart-comics$ ab -n 10000 -c 2 http://localhost:8000/scripts/web/main.dart
...
Requests per second:    1276.72 [#/sec] (mean)
Time per request:       1.567 [ms] (mean)
Yikes! My optimized solutions is 18% slower. That is not good.

By way of comparison, the node.js version (running express.js) has the following performance numbers:
cstrom@londo:~/repos/dart-comics$ ab -n 10000 -c 2 http://localhost:3000/scripts/web/main.dart
...
Requests per second:    1602.96 [#/sec] (mean)
Time per request:       1.248 [ms] (mean)
That is a full 20% faster than the equivalent node.js solution. I'm not too fussed about the slowness between node and Dart. Node has had many, many releases in which performance was the only focus. I am unsure if Dart, in its infancy, has had any.

In retrospect, I should have realized that creating session data like that would be expensive. Although it was a useful exercise to determine how to pass data between route matcher and route responder, it was poor of me to assume it would be an optimization. I should have verified.

Before quitting for the night, I make one last attempt to close the gap with node.js. Instead of storing the path in session, I store it in a local variable. Since the Dart server is single-threaded, this should be safe:
  var last_path;

  app.addRequestHandler(
    (req) {
      if (req.method != 'GET') return false;

      String path = publicPath(req.path);
      if (path == null) return false;

      last_path = path;
      return true;
    },
    (req, res) {
      var file = new File(last_path);
      var stream = file.openInputStream();
      stream.pipe(res.outputStream);
    }
  );
The result? Only modest improvement.
Requests per second:    1566.07 [#/sec] (mean)
Time per request:       1.277 [ms] (mean)
It seems that blocking file-exists check does not block for very long after all. Something to file away for future reference.



Day #569

2 comments:

  1. Creating a session in Dart's HTTP server means generating cryptographically random session ID (on Linux, it reads from /dev/urandom). That isn't exactly a fast operation. I'm not sure if it can be slower than cached file existence check, you might try to replace that bit by something faster and less random and see.

    ReplyDelete
    Replies
    1. Ah, that makes sense. I saw the random session ID the day before when inspecting headers so that fits.

      I could find no other way to communicate between matcher and responder other than session or a block-scope variable like last_path. I do wonder if node or express maintains an in-memory cache to speed this kind of thing up. I should investigate that or compare with a pure-node implementation to get a more apples-to-apples comparison.

      Delete