Skip to content

Instantly share code, notes, and snippets.

@ForbesLindesay
Last active December 17, 2015 21:59
Show Gist options
  • Save ForbesLindesay/5678980 to your computer and use it in GitHub Desktop.
Save ForbesLindesay/5678980 to your computer and use it in GitHub Desktop.

This is an attempt at getting a promise library to run as fast as native callbacks while still offering Promises/A+ compatability. It was sparked by these tweets:

@izs

We would not give up perf in core. You can already do this in userland if you have diff priorities.

@forbeslindesay:

It would take some work to optimise and you couldn’t use something like Q, but I’d be surprised if it couldn’t be done.

@medikoo:

node promises will have to be as fast as "Base" (current callbacks) in this benchmark

It's intended as a proof of concept, The .then method isn't quite good enough to pass the Promises/A+ test suite and crucially it doesn't have any way of handling non-truthy errors. I also haven't spent any time testing it, but it is principally possible to create a then method that is compliant except for the flasy errors issue on top of this implimentaiton.

Note that I've not actually used the .then method, instead I've created a much more optimised .nodeify method. Anyone who uses promises seriously as promises will of course get the slight performance drop of using a real .then method (probably a perf drop of about 30% - 40%). The point I wanted to make though is that you can make the .then method available while still providing a high performance alternative for those who don't have complex control flow problems to manage.

function Promise(fn) {
var resolved = false
var err = null
var res = null
var callbacks = []
function resolve(e, r) {
if (resolved) return
resolved = true
err = e
res = r
for (var i = 0, len = callbacks.length; i < len; i++)
callbacks[i](err, res)
callbacks = null
}
this.nodeify = nodeify
function nodeify(callback) {
if (!callback) return this
if (!resolved) {
callbacks.push(callback)
} else {
callback(err, res)
}
}
try { fn(resolve) }
catch(e) { resolve(e) }
}
Promise.prototype.then = function (onFulfilled, onRejected) {
var self = this
return new Promise(function (callback) {
setImmediate(funciton () {
self.nodeify(function (err, res) {
var handle = (err ? onRejected : onFulfilled)
if (typeof handle != 'function') return callback(err, res)
var result
try {
result = handle(err || res)
} catch (ex) {
callback(ex)
return
}
if (result && (typeof result === 'funciton' || typeof result === 'object')
&& typeof result.then === 'funciton') {
result.then(function (res) {
callback(undefined, res)
}, callback)
} else {
callback(null, result)
}
})
})
})
}
var Promise = require('./node-promise.js')
var forEach = require('es5-ext/lib/Object/for-each')
, pad = require('es5-ext/lib/String/prototype/pad')
, lstat = require('fs').lstat
, deferred = require('deferred')
, now = Date.now
, Deferred = deferred.Deferred, promisify = deferred.promisify
, nextTick = process.nextTick
, self, time, count = 100000, data = {}, next, tests, def = deferred();
console.log("Promise overhead (calling one after another)",
"x" + count + ":\n");
tests = [function () {
function rstat(filename, callback) {
return lstat(filename, function (err, res) {
callback(err, res)
})
}
var i = count;
self = function () {
rstat(__filename, function (err, stats) {
if (err) {
throw err;
}
if (--i) {
self(stats);
} else {
data["Base (plain Node.js lstat call)"] = now() - time;
next();
}
});
};
time = now();
self();
}, function () {
function rstat(filename) {
return new Promise(function (callback) {
lstat(filename, callback)
})
}
var i = count;
self = function () {
rstat(__filename).nodeify(function (err, stats) {
if (err) {
return console.log(err.stack)
}
if (--i) {
self(stats);
} else {
data["Promise"] = now() - time;
next();
}
})
};
time = now();
self();
}];
next = function () {
if (tests.length) {
tests.shift()();
} else {
def.resolve();
forEach(data, function (value, name, obj, index) {
console.log(index + 1 + ":", pad.call(value, " ", 5) + "ms ", name);
}, null, function (a, b) {
return this[a] - this[b];
});
}
};
next();
Promise overhead (calling one after another) x100000:

1:  7125ms  Base (plain Node.js lstat call)  fastest
2:  7188ms  Promise                          0.88% slower

So as you can see, I didn't quite manage it. That difference is very close to completely negligable though. If we assume that I'm not the best performance optomiser ever to live on this earth (a very reasonable assumption) then it's probably possible to close the gap even further.

@medikoo
Copy link

medikoo commented May 30, 2013

@ForbseyLindsay that way is not possible to get 1:1, as we always end up writing a wrapper over original callback.

It has to be done by hacking Node internals. Let's not handle callback but return promise from original lstat, then see if we can make lstat(path).done(cb) as fast as original lstat(path, cb).

I believe it will require some work in C++ layer, and even if we get 1:1 in this benchmark, it's good to check whether in general, creation of thousands of promise objects (instead of just passing values to callback) doesn't introduce any perf problem (memory consumption ?)

@ForbesLindesay
Copy link
Author

I agree, work almost certainly needs to be done by hacking node core (perhaps even a C++ implementation?). Unfortunately my knowledge of C++ is extremely limited and my desire to fork node.js and then maintain my own fork is even lower.

I actually think the aim should be to ensure that lstat(path, cb) in the version that supports promises is as fast as lstat(path, cb) in a version that does not support promises but that you can still do lstat(path).then(... and get a real promise.

We need to ensure that it is no slower for callback users, not that promise users see no slow down.

For example:

function () {
  function rstat(filename, callback) {
    if (callback === undefined) {
      return new Promise(function (callback) {
        rstat(filename, function (err, res) {
          callback(err, res)
        })
      })
    }
    return lstat(filename, callback)
  }
  var i = count;
  self = function () {
    rstat(__filename, function (err, stats) {
      if (err) {
        throw err;
      }
      if (--i) {
        self(stats);
      } else {
        data["Promise skipped"] = now() - time;
        next();
      }
    });
  };
  time = now();
  self();
}

satisfies the requirements of returning a promise when no callback is passed in, and is an almost perfect match for performance when compared with the pure callback version. It's a bit of an ugly hack, but there you have it, it does work.

@medikoo
Copy link

medikoo commented May 30, 2013

Indeed creation of promise only when callback is not provided, leaves the door to maintain current performance in existing scripts, that's neat, but on the other hand it feels hacky and dirty, so not acceptable for node.

To have a clean and concise API I'd say that promise should always be returned from async function, and callback support should be kept for legacy reasons, so compatibility is not broken.

Maybe this change will be taken into account if promises really win as async standard for JS. Adoption is growing, but still tools like async are more popular, so we're not there yet ;-)

@ForbesLindesay
Copy link
Author

We're not there yet, but I couldn't be much more certain that they will win in the long run. They've already achieved that status in C#. We're getting really powerful keyword support that's just around the corner for node.js and only a few years down the line before we can start using it in browsers. TC39 is strongly in favor of it in some form or other so the language will almost certainly end up with a native Promise object. DOM Future is happening, it's good stuff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment