Skip to content

leak from plugin when using toString on large data #401

Open
@psnider

Description

@psnider

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 !

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions