Description
I've encountered a severe memory leak when an external service started sending very large XML responses (~40MB).
Note that there are some bug reports in node with very large strings leaking, but they must be around 250MB, and seem to be unrelated to this issue. Also , I couldn't reproduce this problem when I don't use seneca.
node v5.0.0
seneca ^1.3.0
Here is minimal code I use from a REPL that reproduces the issue reliably.
Enable garbage collection API to help with testing:
node --expose-gc
// if true, toString() is called within the plugin code, if false, it is called within the triggering act callback
var perform_toString_within_plugin
function generateLargeBuffer() {
const buf = new Buffer(43632013)
buf.fill(0)
return buf
}
/////// SIMPLE PLUGLIN
var fs = require('fs')
function leaking_plugin( options ) {
const PLUGIN_NAME = 'leaking'
this.add('role:test,cmd:leak', (msg, respond) => {
var data = generateLargeBuffer()
if (perform_toString_within_plugin) {
var contents = data.toString()
console.log('toString() in leaking_plugin')
} else {
var contents = data
}
respond(null, {contents})
})
this.add( {init: PLUGIN_NAME}, function( args, done ) {
done()
})
return PLUGIN_NAME
}
function senecaAction(msg, done) {
seneca.act(msg, (error, response) => {
if (!error) {
if (perform_toString_within_plugin) {
var s = response.contents
} else {
s = response.contents.toString()
console.log('toString() in senecaAction()')
}
console.log('OK to process data')
} else {
console.log('error=' + error);
}
done(error, s)
})
}
fs = require('fs')
SENECA = require('seneca')
var seneca = SENECA({
"log": "silent",
"default_plugins": {
"mem-store": true
},
"debug": {
"undead": true
},
"timeout": 99999
})
seneca.use(leaking_plugin)
seneca.error((error) => {
console.log('Seneca error: ' + error)
})
function test() {
senecaAction({role: 'test',cmd: 'leak'}, function(error, s) {
console.log('test finished...')
})
}
Find the base heap size:
global.gc()
process.memoryUsage()
For me this gives: { rss: 52281344, heapTotal: 32966400, heapUsed: 26,996,888 }
Then excercise the above code from the REPL with combinations of tests and measurements and running the GC.
perform_toString_within_plugin = false
test()
test()
test()
test()
Wait for the tests to finish, then run:
process.memoryUsage()
For me this gives: { rss: 293875712, heapTotal: 152241920, heapUsed: 115,433,200 }
Verify that this memory can be reclaimed by running the GC:
global.gc()
process.memoryUsage()
For me this gives: { rss: 182329344, heapTotal: 31946496, heapUsed: 26,413,976 }
So it appears that memory is reclaimed correctly.
Now switch to performing the toString() from within the plugin:
perform_toString_within_plugin = true
test()
test()
test()
test()
Again, wait for the tests to finish, then run:
process.memoryUsage()
For me this gives: { rss: 527720448, heapTotal: 206558208, heapUsed: 198,439,760 }
Now confirm that the GC cannot reclaim this memory any longer.
global.gc()
process.memoryUsage()
For me this gives: { rss: 523902976, heapTotal: 204523008, heapUsed: 198,208,656 }
In contrast to before, it has dropped only by about 200k !