From 7447767c73c8f6aed83c6fb3feee24996b3bf2e9 Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Sun, 24 May 2015 23:58:28 -0700 Subject: [PATCH 1/4] initial ensureAsync implementation --- lib/async.js | 22 +++++++++++++++++ perf/benchmark.js | 6 ++++- perf/suites.js | 24 +++++++++++++++++++ test/test-async.js | 60 ++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 107 insertions(+), 5 deletions(-) diff --git a/lib/async.js b/lib/async.js index 4257f0de5..86c810a3a 100644 --- a/lib/async.js +++ b/lib/async.js @@ -1282,4 +1282,26 @@ root.async = async; } + function ensureAsync(fn) { + return function (/*...args, callback*/) { + var args = _baseSlice(arguments); + var callback = args.pop(); + args.push(function () { + var innerArgs = arguments; + if (sync) { + async.setImmediate(function () { + callback.apply(null, innerArgs); + }); + } else { + callback.apply(null, innerArgs); + } + }); + var sync = true; + fn.apply(this, args); + sync = false; + }; + } + + async.ensureAsync = ensureAsync; + }()); diff --git a/perf/benchmark.js b/perf/benchmark.js index da5921647..9e57fd961 100755 --- a/perf/benchmark.js +++ b/perf/benchmark.js @@ -129,7 +129,10 @@ function createSuite(suiteConfig) { }); }, _.extend({ versionName: versionName, - setup: _.partial.apply(null, [suiteConfig.setup].concat(args)) + setup: _.partial.apply(null, [suiteConfig.setup].concat(args)), + onError: function (err) { + console.log(err.stack); + } }, benchOptions)); } @@ -143,6 +146,7 @@ function createSuite(suiteConfig) { var version = event.target.options.versionName; totalTime[version] += mean; }) + .on('error', function (err) { console.error(err); }) .on('complete', function() { var fastest = this.filter('fastest'); if (fastest.length === 2) { diff --git a/perf/suites.js b/perf/suites.js index 9a36db53b..28b5a3236 100644 --- a/perf/suites.js +++ b/perf/suites.js @@ -204,6 +204,30 @@ module.exports = [ fn: function (async, done) { setTimeout(done, 0); } + }, + { + name: "ensureAsync sync", + fn: function (async, done) { + async.ensureAsync(function (cb) { + cb(); + })(done); + } + }, + { + name: "ensureAsync async", + fn: function (async, done) { + async.ensureAsync(function (cb) { + setImmediate(cb); + })(done); + } + }, + { + name: "ensureAsync async noWrap", + fn: function (async, done) { + (function (cb) { + setImmediate(cb); + }(done)); + } } ]; diff --git a/test/test-async.js b/test/test-async.js index 81c178700..6797afeff 100755 --- a/test/test-async.js +++ b/test/test-async.js @@ -1030,12 +1030,12 @@ exports['parallel does not continue replenishing after error'] = function (test) } setTimeout(function(){ callback(); - }, delay); + }, delay); } async.parallelLimit(arr, limit, function(x, callback) { - }, function(err){}); + }, function(err){}); setTimeout(function(){ test.equal(started, 3); @@ -1438,7 +1438,7 @@ exports['eachLimit does not continue replenishing after error'] = function (test setTimeout(function(){ callback(); }, delay); - }, function(err){}); + }, function(err){}); setTimeout(function(){ test.equal(started, 3); @@ -1743,7 +1743,7 @@ exports['mapLimit does not continue replenishing after error'] = function (test) setTimeout(function(){ callback(); }, delay); - }, function(err){}); + }, function(err){}); setTimeout(function(){ test.equal(started, 3); @@ -3561,3 +3561,55 @@ exports['queue started'] = function(test) { }; +exports['ensureAsync'] = { + 'defer sync functions': function (test) { + var sync = true; + async.ensureAsync(function (arg1, arg2, cb) { + test.equal(arg1, 1); + test.equal(arg2, 2); + cb(null, 4, 5); + })(1, 2, function (err, arg4, arg5) { + test.equal(err, null); + test.equal(arg4, 4); + test.equal(arg5, 5); + test.ok(!sync, 'callback called on same tick'); + test.done(); + }); + sync = false; + }, + + 'do not defer async functions': function (test) { + var sync = false; + async.ensureAsync(function (arg1, arg2, cb) { + test.equal(arg1, 1); + test.equal(arg2, 2); + async.setImmediate(function () { + sync = true; + cb(null, 4, 5); + sync = false; + }); + })(1, 2, function (err, arg4, arg5) { + test.equal(err, null); + test.equal(arg4, 4); + test.equal(arg5, 5); + test.ok(sync, 'callback called on next tick'); + test.done(); + }); + }, + + 'double wrapping': function (test) { + var sync = true; + async.ensureAsync(async.ensureAsync(function (arg1, arg2, cb) { + test.equal(arg1, 1); + test.equal(arg2, 2); + cb(null, 4, 5); + }))(1, 2, function (err, arg4, arg5) { + test.equal(err, null); + test.equal(arg4, 4); + test.equal(arg5, 5); + test.ok(!sync, 'callback called on same tick'); + test.done(); + }); + sync = false; + } +}; From 219b4fbc93cb3c8c0a1bbdd7e58028d6087d1c73 Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Mon, 25 May 2015 14:40:17 -0700 Subject: [PATCH 2/4] handle errors in benchmarks --- perf/benchmark.js | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/perf/benchmark.js b/perf/benchmark.js index 9e57fd961..5cd871b87 100755 --- a/perf/benchmark.js +++ b/perf/benchmark.js @@ -2,7 +2,6 @@ var _ = require("lodash"); var Benchmark = require("benchmark"); -var benchOptions = {defer: true, minSamples: 1, maxTime: 2}; var exec = require("child_process").exec; var fs = require("fs"); var path = require("path"); @@ -16,8 +15,11 @@ var args = require("yargs") .alias("g", "grep") .default("g", ".*") .describe("i", "skip benchmarks whose names match this regex") - .alias("g", "reject") + .alias("i", "reject") .default("i", "^$") + .describe("l", "maximum running time per test (in seconds)") + .alias("l", "limit") + .default("l", 2) .help('h') .alias('h', 'help') .example('$0 0.9.2 0.9.0', 'Compare v0.9.2 with v0.9.0') @@ -33,6 +35,7 @@ var reject = new RegExp(args.i, "i"); var version0 = args._[0] || require("../package.json").version; var version1 = args._[1] || "current"; var versionNames = [version0, version1]; +var benchOptions = {defer: true, minSamples: 1, maxTime: +args.l}; var versions; var wins = {}; var totalTime = {}; @@ -120,9 +123,20 @@ function doesNotMatch(suiteConfig) { function createSuite(suiteConfig) { var suite = new Benchmark.Suite(); var args = suiteConfig.args; + var errored = false; function addBench(version, versionName) { var name = suiteConfig.name + " " + versionName; + + try { + suiteConfig.setup(1); + suiteConfig.fn(version, function () {}); + } catch (e) { + console.error(name + " Errored"); + errored = true; + return; + } + suite.add(name, function (deferred) { suiteConfig.fn(version, function () { deferred.resolve(); @@ -142,19 +156,22 @@ function createSuite(suiteConfig) { return suite.on('cycle', function(event) { var mean = event.target.stats.mean * 1000; - console.log(event.target + ", " + (+mean.toPrecision(2)) + "ms per run"); + console.log(event.target + ", " + (+mean.toPrecision(3)) + "ms per run"); var version = event.target.options.versionName; + if (errored) return; totalTime[version] += mean; }) .on('error', function (err) { console.error(err); }) .on('complete', function() { - var fastest = this.filter('fastest'); - if (fastest.length === 2) { - console.log("Tie"); - } else { - var winner = fastest[0].options.versionName; - console.log(winner + ' is faster'); - wins[winner]++; + if (!errored) { + var fastest = this.filter('fastest'); + if (fastest.length === 2) { + console.log("Tie"); + } else { + var winner = fastest[0].options.versionName; + console.log(winner + ' is faster'); + wins[winner]++; + } } console.log("--------------------------------------"); }); From aba3e0c1af87f0f89bbddb6d3a2c56ffb9176c9b Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Mon, 25 May 2015 14:58:27 -0700 Subject: [PATCH 3/4] docs for ensureAsync --- README.md | 36 ++++++++++++++++++++++++++++++++++++ lib/async.js | 30 +++++++++++++++--------------- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d09f9f0b7..eadf80158 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ Usage: * [`memoize`](#memoize) * [`unmemoize`](#unmemoize) +* [`ensureAsync`](#ensureAsync) * [`log`](#log) * [`dir`](#dir) * [`noConflict`](#noConflict) @@ -1657,6 +1658,41 @@ __Arguments__ * `fn` - the memoized function +--------------------------------------- + + +### ensureAsync(fn) + +Wrap an async function and ensure it calls its callback on a later tick of the event loop. If the function already calls its callback on a next tick, no extra deferral is added. This is useful for preventing stack overflows (`RangeError: Maximum call stack size exceeded`) and generally keeping [Zalgo](http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony) contained. + +__Arguments__ + +* `fn` - an async function, one that expects a node-style callback as its last argument + +Returns a wrapped function with the exact same call signature as the function passed in. + +__Example__ + +```js +function sometimesAsync(arg, callback) { + if (cache[arg]) { + return callback(null, cache[arg]); // this would be synchronous!! + } else { + doSomeIO(arg, callback); // this IO would be asynchronous + } +} + +// this has a risk of stack overflows if many results are cached in a row +async.mapSeries(args, sometimesAsync, done); + +// this will defer sometimesAsync's callback if necessary, +// preventing stack overflows +async.mapSeries(args, async.ensureAsync(sometimesAsync), done); + +``` + +--------------------------------------- + ### log(function, arguments) diff --git a/lib/async.js b/lib/async.js index 86c810a3a..06ede1475 100644 --- a/lib/async.js +++ b/lib/async.js @@ -1267,21 +1267,6 @@ next(); }; - // Node.js - if (typeof module !== 'undefined' && module.exports) { - module.exports = async; - } - // AMD / RequireJS - else if (typeof define !== 'undefined' && define.amd) { - define([], function () { - return async; - }); - } - // included directly via