Search code examples
node.jsmemory-managementsetintervalmjpeg

Node.js: Memory usage keeps going up


We are writing a script that reads a large set of JPG files on our server (infinite, since we have another process that keeps writing JPG files to the same directory) and send them to users' browsers as an MJPEG stream at fixed time interval (variable "frameDelay" in code below). This is similar to what an IP camera would do.

We found out the memory usage of this script keeps going up and always ends up being killed by the system (Ubuntu);

We have inspected this seemingly simple script many many times. Therefore I'm posting the code below. Any comments / suggestions are greatly appreciated!

app.get('/stream', function (req, res) {
    res.writeHead(200, {
        'Content-Type':'multipart/x-mixed-replace;boundary="' + boundary + '"',
        'Transfer-Encoding':'none',
        'Connection':'keep-alive',
        'Expires':'Fri, 01 Jan 1990 00:00:00 GMT',
        'Cache-Control':'no-cache, no-store, max-age=0, must-revalidate',
        'Pragma':'no-cache'
    });
res.write(CRLF + "--" + boundary + CRLF);

setInterval(function () {
    if(fileList.length<=1){
        fileList = fs.readdirSync(location).sort();
    }else{
        var fname = fileList.shift();
        if(fs.existsSync(location+fname)){
           var data = fs.readFileSync(location+fname);
            res.write('Content-Type:image/jpeg' + CRLF + 'Content-Length: ' + data.length + CRLF + CRLF);
            res.write(data);
            res.write(CRLF + '--' + boundary + CRLF);                   
            fs.unlinkSync(location+fname);
        }else{
            console.log("File doesn't find")
        }
    }
        console.log("new response:" + fname);
    }, frameDelay);
});

app.listen(port);
console.log("Server running at port " + port);

To facilitate the troubleshooting process, below is a stand-alone (no 3rd-party lib) test case.

It has exactly the same memory issue (memory usage keeps going up and finally got killed by the OS).

We believe the problem is in the setInterval () loop - maybe those images didn't get deleted from memory after being sent or something (maybe still stored in variable "res"?).

Any feedback / suggestions are greatly appreciated!

var http                = require('http');
var fs                  = require('fs');

var framedelay  = 40;
var port                = 3200;
var boundary    = 'myboundary';
var CR                  = '\r';
var LF                  = '\n';
var CRLF                = CR + LF;

function writeHttpHeader(res)
{
        res.writeHead(200,
        {
                'Content-Type': 'multipart/x-mixed-replace;boundary="' + boundary + '"',
                'Transfer-Encoding': 'none',
                'Connection': 'keep-alive',
                'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT',
               'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
                'Pragma': 'no-cache',
        });

        res.write(CRLF + '--' + boundary + CRLF);
}

function writeJpegFrame(res, filename)
{
        fs.readFile('./videos-8081/frames/' + filename, function(err, data)
        {
                if (err)
                {
                        console.log(err);
                }
                else
                {
                        res.write('Content-Type:image/jpeg' + CRLF);
                        res.write('Content-Length:' + data.length + CRLF + CRLF);
                            res.write(data);
                        res.write(CRLF + '--' + boundary + CRLF);
                        console.log('Sent ' + filename);
                }
        });
}
http.createServer(function(req, res)
{
     writeHttpHeader(res)    
     fs.readdir('./videos-8081/frames', function(err, files)
     { 
         var i = -1;
         var sorted_files = files.sort();    
         setInterval(function()
         {
              if (++i >= sorted_files.length)
              {
                i = 0;
              }    
              writeJpegFrame(res, sorted_files[i]);
          }, framedelay);
        });
}).listen(port);
console.log('Server running at port ' + port);                    

Solution

  • There are a few things caused this

    • MJPEG is not designed to send high resolution motion pictures in a high frequency (25fps, in your case, may be ok for 320x240 frames, but not for 720p.) Just consider the output throughput of the payload 25fps*70KB = 1750KBps = 14Mbps which is higher than full HD video.
    • Node.js will cache the output in a buffer when the client is incapable to receive. Because you are sending to much data to client, so node saved them for you. That's why your memory usage never go down, and it's NOT memory leak. To detect if the output buffer is bulked up, check the return value of res.write().
    • setInterval() is OK to use, and will not cause any problem as long as the client keep the connection. But when the client is disconnected, you need to stop it. To do so, you need to monitor 'close' event.
    • You cannot maintain a stable fps by using MJPEG as it's not designed for this purpose, so no matter how hard you try, you cannot control the fps at client. But with carefully designed code, you can make the average fps stable by using setTimeout().

    Here is the fixed code.

    var http = require('http');
    var fs = require('fs');
    var framedelay = 40;
    var port = 3200;
    var boundary = 'myboundary';
    var CR = '\r';
    var LF = '\n';
    var CRLF = CR + LF;
    
    http.createServer(function(req, res) {
    
      var files = fs.readdirSync('./imgs');
      var i = -1;
      var timer;
      var sorted_files = files.sort();
    
      res.writeHead(200, {
        'Content-Type': 'multipart/x-mixed-replace;boundary="' + boundary + '"',
        'Transfer-Encoding': 'none',
        'Connection': 'keep-alive',
        'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT',
        'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
        'Pragma': 'no-cache',
      });
    
      res.write(CRLF + '--' + boundary + CRLF);
    
      var writePic = function() {
    
        if (++i >= sorted_files.length)
          i = 0;
    
        var data = fs.readFileSync('./imgs/' + sorted_files[i]);
        res.write('Content-Type:image/jpeg' + CRLF);
        res.write('Content-Length:' + data.length + CRLF + CRLF);
        res.write(data);
        var ok = res.write(CRLF + '--' + boundary + CRLF);
        console.log('Sent ' + sorted_files[i], ok);
    
        if (ok)
          timer = setTimeout(writePic, framedelay);
      };
    
      res.on('close', function() {
        console.log('client closed');
        clearTimeout(timer);
      });
    
      res.on('drain', function() {
        console.log('drain');
        timer = setTimeout(writePic, framedelay);
      });
    
    }).listen(port);
    
    console.log('Server running at port ' + port);