diff --git a/package.json b/package.json index 244376c..015d4f8 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "supertest": "~0.15.0" }, "scripts": { - "test": "./node_modules/.bin/mocha ./test/test.js --reporter spec" + "test": "./node_modules/.bin/mocha ./test/test*.js --reporter spec" }, "engines": { "node": ">=0.6.6", diff --git a/test/child.js b/test/child.js new file mode 100644 index 0000000..c95fc0a --- /dev/null +++ b/test/child.js @@ -0,0 +1,64 @@ +// When this module is loaded, CORS Anywhere is started. +// Then, a request is generated to warm up the server (just in case). +// Then the base URL of CORS Anywhere is sent to the parent process. +// ... +// When the parent process is done, it sends an empty message to this child +// process, which in turn records the change in used heap space. +// The difference in heap space is finally sent back to the parent process. +// ... +// The parent process should then kill this child. + +process.on('uncaughtException', function(e) { + console.error('Uncaught exception in child process: ' + e); + console.error(e.stack); + process.exit(-1); +}); + +// Invoke memoryUsage() without using its result to make sure that any internal +// datastructures that supports memoryUsage() is initialized and won't pollute +// the memory usage measurement later on. +process.memoryUsage(); + +var heapUsedStart = 0; +function getMemoryUsage(callback) { + // Note: Requires --expose-gc + // 6 is the minimum amount of gc() calls before calling gc() again does not + // reduce memory any more. + for (var i = 0; i < 6; ++i) { + global.gc(); + } + callback(process.memoryUsage().heapUsed); +} + +var server; +if (process.argv.indexOf('use-http-instead-of-cors-anywhere') >= 0) { + server = require('http').createServer(function(req, res) { res.end(); }); +} else { + server = require('../').createServer(); +} + +server.listen(0, function() { + // Perform 1 request to warm up. + require('http').get({ + hostname: '127.0.0.1', + port: server.address().port, + path: '/http://invalid:99999', + agent: false, + }, function() { + notifyParent(); + }); + + function notifyParent() { + getMemoryUsage(function(usage) { + heapUsedStart = usage; + process.send('http://127.0.0.1:' + server.address().port + '/'); + }); + } +}); + +process.once('message', function() { + getMemoryUsage(function(heapUsedEnd) { + var delta = heapUsedEnd - heapUsedStart; + process.send(delta); + }); +}); diff --git a/test/test-memory.js b/test/test-memory.js new file mode 100644 index 0000000..4def37a --- /dev/null +++ b/test/test-memory.js @@ -0,0 +1,110 @@ +// Run this specific test using: +// npm test -- -f memory +var http = require('http'); +var path = require('path'); +var url = require('url'); +var fork = require('child_process').fork; + +describe('memory usage', function() { + var cors_api_url; + + var server; + var cors_anywhere_child; + before(function(done) { + server = http.createServer(function(req, res) { + res.writeHead(200); + res.end(); + }).listen(0, function() { + done(); + }); + }); + + after(function(done) { + server.close(function() { + done(); + }); + }); + + beforeEach(function(done) { + var cors_module_path = path.join(__dirname, 'child'); + var args = []; + // Uncomment this if you want to compare the performance of CORS Anywhere + // with the standard no-op http module. + // args.push('use-http-instead-of-cors-anywhere'); + cors_anywhere_child = fork(cors_module_path, args, { + execArgv: ['--expose-gc'], + }); + cors_anywhere_child.once('message', function(cors_url) { + cors_api_url = cors_url; + done(); + }); + }); + + afterEach(function() { + cors_anywhere_child.kill(); + }); + + /** + * Perform N CORS Anywhere proxy requests to a simple test server. + * + * @param {number} n - number of repetitions. + * @param {number} requestSize - Approximate size of request in kilobytes. + * @param {number} memMax - Expected maximum memory usage in kilobytes. + * @param {function} done - Upon success, called without arguments. + * Upon failure, called with the error as parameter. + */ + function performNRequests(n, requestSize, memMax, done) { + var remaining = n; + var request = url.parse( + cors_api_url + 'http://127.0.0.1:' + server.address().port); + request.agent = false; // Force Connection: Close + request.headers = { + 'Long header': new Array(requestSize * 1e3).join('x'), + }; + (function requestAgain() { + if (remaining-- === 0) { + cors_anywhere_child.once('message', function(memory_usage_delta) { + console.log('Memory usage delta: ' + memory_usage_delta + + ' (' + n + ' requests of ' + requestSize + ' kb each)'); + if (memory_usage_delta > memMax * 1e3) { + // Note: Even if this error is reached, always profile (e.g. using + // node-inspector) whether it is a true leak, and not e.g. noise + // caused by the implementation of V8/Node.js. + // Uncomment args.push('use-http-instead-of-cors-anywhere') at the + // fork() call to get a sense of what's normal. + throw new Error('Possible memory leak: ' + memory_usage_delta + + ' bytes was not released, which exceeds the ' + memMax + + ' kb limit by ' + + Math.round(memory_usage_delta / memMax / 10 - 100) + '%.'); + } + done(); + }); + cors_anywhere_child.send(null); + return; + } + http.request(request, function() { + requestAgain(); + }).on('error', function(error) { + done(error); + }).end(); + })(); + } + + it('100 GET requests 50k', function(done) { + // This test is just for comparison with the following tests. + performNRequests(100, 50, 550, done); + }); + + // 100x 1k and 100x 50k for comparison. + // Both should use about the same amount of memory if there is no leak. + it('1000 GET requests 1k', function(done) { + // Every request should complete within 10ms. + this.timeout(1000 * 10); + performNRequests(1000, 1, 2000, done); + }); + it('1000 GET requests 50k', function(done) { + // Every request should complete within 10ms. + this.timeout(1000 * 10); + performNRequests(1000, 50, 2000, done); + }); +});