blob: 0931fe83dd970830bb7d92d174a256ed62721b8f [file] [log] [blame]
'use strict';
require('chai').should();
var default_opts = {
/* This module can work in regular "test" mode, but it may also dump
* heap so that you can analyze what is going wrong.
* In the later case, simply set the following variable to `true`.
*/
dump_snapshots: process.env.DUMP_HEAP === 'true' ? true : false,
/* How much growth is too much growth?
* In practice I can't get Node.js to not leak even with the simple
* classes defined below (maybe they are just plain buggy, but most
* of what heapdump reveals leads to nextTickQueue, so I suspect
* this is inherent to the way the tests are ran).
* Therefor we define the following as the maximum size and maximum
* percentage growth we will accept as "normal".
*/
max_growth: 50000, // Up to 50ko lost in heap
max_percent: 1, // Up to 1% lost in heap
/* How many times should the test run? */
runs: 10000
};
var strict_opts = {
dump_snapshots: default_opts.dump_snapshots,
max_growth: 0,
max_percent: 0,
runs: 10000
};
/* A dummy adapter for test purposes. */
function DummyPouchPlugin(PouchDB) {
function DummyPouchAdapter(opts,callback) {
var api = this;
api.close = function DummyPouchAdapterClose(callback) {
this.emit('closed');
if (callback) {
callback(null,null);
} else {
return Promise.resolve();
}
};
api.info = function DummyPouchAdapterInfo(callback) {
if (callback) {
callback(null,{dummy:true});
} else {
return Promise.resolve({dummy:true});
}
};
if (callback) {
callback(null,api);
} else {
return Promise.resolve(api);
}
}
DummyPouchAdapter.valid = function DummyPouchAdapterValid() { return true; };
PouchDB.adapter('dummy', DummyPouchAdapter, false);
}
/* A dummy adapter that extends the core AbstractPouchDB class. */
function SomewhatDummyPouchPlugin(PouchDB) {
function SomewhatDummyPouchAdapter(opts,callback) {
var api = this;
api._close = function SomewhatDummyPouchAdapterClose(callback) {
if (callback) {
callback(null,null);
} else {
return Promise.resolve();
}
};
api._info = function SomewhatDummyPouchAdapterInfo(callback) {
if (callback) {
callback(null,{dummy:true});
} else {
return Promise.resolve({dummy:true});
}
};
if (callback) {
callback(null,api);
} else {
return Promise.resolve(api);
}
}
SomewhatDummyPouchAdapter.valid = function SomwehatDummyPouchAdapterValid() { return true; };
PouchDB.adapter('somewhatdummy', SomewhatDummyPouchAdapter, false);
}
/* A fake PouchDB, used to make sure the leak detection code works. */
var FakePouchDB;
FakePouchDB = (function () {
var adapters = {};
function FakePouchDBAPI() {
}
FakePouchDBAPI.prototype.emit = function () {};
function FakePouchDB(location) {
var type = location.split(':')[0];
this.opts = {};
this.adapter = adapters[type];
}
FakePouchDB.adapter = function (type,adapter) {
if (adapter.valid()) {
adapters[type] = adapter;
}
};
FakePouchDB.plugin = function (plugin) {
plugin(this);
};
FakePouchDB.prototype.api = function () {
var self = this;
return new Promise( function (resolve,reject) {
var empty_api = new FakePouchDBAPI();
self.adapter.call(empty_api, self.opts, function (err,api) {
if (err) {
console.log("Error: "+(err.stack || err));
reject(err);
} else {
resolve(api);
}
});
});
};
FakePouchDB.prototype.info = function () {
return this.api().then(function (api) {return api.info();});
};
FakePouchDB.prototype.close = function () {
return this.api().then(function (api) {return api.close();});
};
return FakePouchDB;
})();
/* Real PouchDB, for tests */
var PouchDB = require('../../packages/node_modules/pouchdb-for-coverage');
/* Basic sleep functionality for Promises */
function sleep(timeout) {
return new Promise( function (resolve) {
setTimeout(resolve,timeout);
});
}
/* A class to measure and test heap variations over time. */
var MeasureHeap = (function () {
function MeasureHeap(done,opts,dump) {
this.stable_heap = null;
this.done = done;
this.remaining = opts.runs;
this.runs = opts.runs;
this.max_growth = opts.max_growth;
this.max_percent = opts.max_percent;
if (opts.dump_snapshots) {
var heapdump = require('heapdump');
this.dump = function (name) {
return new Promise( function (resolve,reject) {
console.log('Snapshotting to '+dump+name);
heapdump.writeSnapshot( dump+name, function (err,filename) {
if (err) {
console.log("Error in snapshot: "+(err.stack || err));
reject(err);
} else {
resolve(filename);
}
});
});
};
} else {
this.dump = function (name) {
return Promise.resolve(dump+name);
};
}
}
MeasureHeap.prototype.init = function () {
var self = this;
var memory = null;
global.gc();
return sleep(6*1000)
.then( function () {
global.gc();
memory = process.memoryUsage();
return self.dump('-start.heapsnapshot');
}).then( function () {
self.stable_heap = memory.heapUsed;
return false;
});
};
MeasureHeap.prototype.update = function () {
var self = this;
var memory = null;
self.remaining -= 1;
if (self.remaining > 0) {
return Promise.resolve(false);
}
global.gc();
// Keep this part async so that we don't have to account for
// data accumulated in the `done()` callback, etc.
// (If we `return sleep(..).then(..)` then the heap size does
// not match at all the values recorded in the heap dump.)
sleep(6*1000)
.then( function () {
global.gc();
memory = process.memoryUsage();
return self.dump('-final.heapsnapshot');
}).then( function () {
var measured_heap = memory.heapUsed;
var heap_growth = measured_heap - self.stable_heap;
var percent = Math.ceil(100*heap_growth/self.stable_heap);
var msg = 'Difference is '+ heap_growth+' (vs '+self.max_growth+')'+' (+'+percent+'%)'+' (vs '+self.max_percent+'%) ('+Math.ceil(heap_growth/self.runs)+' per iteration).';
console.log(msg);
if (heap_growth <= self.max_growth && percent <= self.max_percent) {
self.done();
} else {
self.done(new Error(msg));
}
});
return sleep(0).then( function () {
return true;
});
};
return MeasureHeap;
})();
var Catcher = function (err) {
console.log('Catcher: '+err.stack || err.toString());
};
function Runner(measure,Run) {
function Init() { return measure.init(); }
function Update() { return measure.update(); }
function Test(done) {
if (done) {
return true;
}
return Run()
.then( Update )
.then( Test, function (err) { Catcher(err); Test(done); });
}
return Run()
.then(Run)
.then(Run)
.then(Run)
.then( Init )
.then( Test, Catcher );
}
/* Test suite */
describe('test.memleak.js: self-test', function () {
before(function () {
this.timeout(2*1000);
if (!global.gc) {
throw new Error('Please try with `mocha --expose-gc tests/component/test.memleak.js`');
}
FakePouchDB.plugin(DummyPouchPlugin);
return sleep(1*1000);
});
it('Test absence of memory leak in empty', function (next) {
this.timeout(25*1000);
var measure = new MeasureHeap(next,strict_opts,'empty');
function Run() {
return Promise.resolve();
}
Runner(measure,Run);
});
it('Test absence of memory leak in reference code', function (next) {
this.timeout(40*1000);
var measure = new MeasureHeap(next,strict_opts,'reference');
function Run() {
var db = new FakePouchDB('dummy://');
function Finally() { return db.close(); }
return db.info()
.then( Finally, Finally );
}
Runner(measure,Run);
});
});
describe('test.memleak.js -- PouchDB core', function () {
before(function () {
this.timeout(5*1000);
if (!global.gc) {
throw new Error('Please try with `mocha --expose-gc tests/component/test.memleak.js`');
}
PouchDB.plugin(DummyPouchPlugin);
PouchDB.plugin(SomewhatDummyPouchPlugin);
return sleep(4*1000);
});
it('Test limited memory leak in PouchDB core (using dummy)', function (next) {
this.timeout(40*1000);
var measure = new MeasureHeap(next,default_opts,'core');
function Run() {
var db = new PouchDB('dummy://');
function Finally() { return db.close(); }
return db.info()
.then( Finally, Finally );
}
Runner(measure,Run);
});
it('Test limited memory leak in PouchDB core', function (next) {
this.timeout(40*1000);
var measure = new MeasureHeap(next,default_opts,'core2');
function Run() {
var db = new PouchDB('somewhatdummy://');
function Finally() { return db.close(); }
return db.info()
.then( Finally, Finally );
}
Runner(measure,Run);
});
it('Test limited memory leak in PouchDB core (many names)', function (next) {
this.timeout(40*1000);
var measure = new MeasureHeap(next,default_opts,'core2');
function Run() {
var db = new PouchDB('somewhatdummy://'+Math.random());
function Finally() { return db.close(); }
return db.info()
.then( Finally, Finally );
}
Runner(measure,Run);
});
});
describe('test.memleak.js -- http adapter', function () {
if (!process.env.COUCH_HOST) {
return;
}
it('Test basic memory leak in PouchDB http adapter', function (next) {
this.timeout(360*1000);
var opts = {
dump_snapshots: default_opts.dump_snapshots,
max_growth: 33000,
max_percent: 1,
runs: 2000
};
var measure = new MeasureHeap(next,opts,'http');
var host = process.env.COUCH_HOST;
function Run() {
var opts = {
ajax: {
pool: false
}
};
var db_name = host+'/goodluck'+Math.random().toString().substr(4,5);
var db = new PouchDB(db_name,opts);
function Finally() { return db.close(); }
db_name = null;
return db.info()
.then( Finally, Finally )
.then(function () {
db = null;
return sleep(20);
});
}
Runner(measure,Run);
});
});
describe('test.memleak.js -- leveldown adapter', function () {
it('Test basic memory leak in PouchDB leveldown adapter', function (next) {
this.timeout(360*1000);
var opts = {
dump_snapshots: default_opts.dump_snapshots,
max_growth: 33000,
max_percent: 1,
runs: 2000
};
var measure = new MeasureHeap(next,opts,'level');
function Run() {
var db = new PouchDB('goodluck');
function Finally() { return db.close(); }
return db.info()
.then( Finally, Finally )
.then(function () {
return sleep(20);
});
}
Runner(measure,Run);
});
});