From f8f718ead806b111e66a228cb54847f67747dba4 Mon Sep 17 00:00:00 2001 From: Rob Wu Date: Sat, 9 May 2015 11:03:16 +0200 Subject: [PATCH] Add tests for memory leaks Using the performNRequests, I collected the following statistics before choosing the maximum allowed "leaked" memory. Node.js 0.12.2, Using the http module ('use-http-instead-of-cors-anywhere'): Memory usage delta: 132800 (100 requests of 50 kb each, 250ms) Memory usage delta: 110144 (100 requests of 1 kb each, 172ms) Memory usage delta: 709936 (1000 requests of 1 kb each, 902ms) Memory usage delta: 865104 (10000 requests of 1 kb each, 7073ms) Memory usage delta: 930416 (100000 requests of 1 kb each, 62856ms) Using CORS Anywhere: Memory usage delta: 356784 (100 requests of 50 kb each, 1004ms) Memory usage delta: 355248 (100 requests of 1 kb each, 641ms) Memory usage delta: 1326856 (1000 requests of 1 kb each, 3338ms) Memory usage delta: 1462584 (10000 requests of 1 kb each, 21186ms) Memory usage delta: 1473624 (100000 requests of 1 kb each, 211202ms) Clearly, there is a small leak, but it is not proportional/linear in terms of the number of requests, so the observed "leak" is probably not an issue. Furthermore, the "leak" also occurs with the plain http module. After setting fixed limits, I ran the tests on Node.js 0.10.25 and observed that the tests failed due to the too low limits, so I incremented the limits (400 -> 550, 1500 -> 2000). --- package.json | 2 +- test/child.js | 64 ++++++++++++++++++++++++++ test/test-memory.js | 110 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 test/child.js create mode 100644 test/test-memory.js 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); + }); +});