Version
v16.17.1
Platform
Linux 5.15.0-52-generic #58~20.04.1-Ubuntu SMP Thu Oct 13 13:09:46 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
Subsystem
zlib
What steps will reproduce the bug?
Code snippet (run with --expose-gc)
const{ exit }=require('process');const{ deflateSync, inflate }=require('zlib');constMEGABYTES=1024*1024;if(!global.gc){console.error('Please run with `node --expose-gc`');exit(1);}constmain=async()=>{process.on('exit',()=>gcAndLogMem('at exit handler'));createAndFreeBuffer(2000*MEGABYTES);constcompressed=compressBuffer(1000*MEGABYTES);// Decompressing or compressing-freeing several times reuses zlib reserved buffor(leti=0;i<3;i++){gcAndLogMem('before decompress');awaitdecompressBuffer(compressed);gcAndLogMem('after decompress');}createAndFreeBuffer(2000*MEGABYTES);gcAndLogMem('before exit');};// Create a very compressible pattern buffer to make sure it is allocated.constcreateFilledBuffer=size=>{constbuf=Buffer.alloc(size);for(leti=0;i<size;i++)buf[i]=Math.floor(i/1000)&0xff;returnbuf;};constcreateAndFreeBuffer=size=>{console.log('create buffer {');letbuf=createFilledBuffer(size);gcAndLogMem(' created buffer');buf=null;gcAndLogMem(' freed buffer');console.log('} create buffer');};constcompressBuffer=size=>{console.log('compress {');letinput=createFilledBuffer(size);gcAndLogMem(' fill input');constcompressed=deflateSync(input);gcAndLogMem(' deflated');input=null;gcAndLogMem(' freed input');console.log('} compress');returncompressed;};constdecompressBuffer=asynccompressed=>{console.log('decompress {');letoutput=null;awaitnewPromise(resolve=>{inflate(compressed,(err,decompressed)=>{output=decompressed;gcAndLogMem('after inflate');resolve();});});gcAndLogMem(' after promise');output=null;gcAndLogMem(' free output');console.log('} decompress');};constgcAndLogMem=m=>{global.gc();global.gc();console.log(`${m.padEnd(16,'.')} GC CALL/EXT MEM= ${(process.memoryUsage().external/MEGABYTES).toFixed(1)}MB`,);};main();How often does it reproduce? Is there a required condition?
Always
What is the expected behavior?
Memory should lower back to near zero after finishing each decompression routine/scope.
What do you see instead?
Memory never lowers back to "zero" until the exit handler, not allowing it's reuse for something else.
It's not a cumulative memory leak, but keeps it reserved forever.
In a system with 2.5 GB memory limit it would crash due to OOM, even if we could fit 2GB easily if that memory held by zlib was released. Try it: (ulimit -Sd 2500000; node --expose-gc ./script.js )
Script output unbounded
$ node --expose-gc script.js
create buffer { created buffer GC CALL/EXT MEM= 2000.3MB freed buffer.. GC CALL/EXT MEM= 0.3MB} create buffercompress { fill input.... GC CALL/EXT MEM= 1000.3MB deflated...... GC CALL/EXT MEM= 1003.3MB freed input... GC CALL/EXT MEM= 3.3MB} compressbefore decompress GC CALL/EXT MEM= 3.3MBdecompress {after inflate... GC CALL/EXT MEM= 2003.4MB after promise. GC CALL/EXT MEM= 2003.4MB free output... GC CALL/EXT MEM= 1003.4MB <--------- this internal buffer is never released} decompressafter decompress GC CALL/EXT MEM= 1003.4MBbefore decompress GC CALL/EXT MEM= 1003.4MBdecompress {after inflate... GC CALL/EXT MEM= 2003.4MB <--------- but it looks like it's reused by zlib after promise. GC CALL/EXT MEM= 2003.4MB free output... GC CALL/EXT MEM= 1003.4MB} decompressafter decompress GC CALL/EXT MEM= 1003.4MBbefore decompress GC CALL/EXT MEM= 1003.4MBdecompress {after inflate... GC CALL/EXT MEM= 2003.4MB after promise. GC CALL/EXT MEM= 2003.4MB free output... GC CALL/EXT MEM= 1003.4MB} decompressafter decompress GC CALL/EXT MEM= 1003.4MBcreate buffer { created buffer GC CALL/EXT MEM= 3003.4MB <------ here crashes with limited memory freed buffer.. GC CALL/EXT MEM= 1003.4MB} create bufferbefore exit..... GC CALL/EXT MEM= 1003.4MBat exit handler. GC CALL/EXT MEM= 0.3MBScript output boudned to 2500 MB (in Ubuntu, YMMV)
( ulimit -Sd 2500000 ; node --expose-gc script.js )
create buffer { created buffer GC CALL/EXT MEM= 2000.3MB freed buffer.. GC CALL/EXT MEM= 0.3MB} create buffercompress { fill input.... GC CALL/EXT MEM= 1000.3MB deflated...... GC CALL/EXT MEM= 1003.3MB freed input... GC CALL/EXT MEM= 3.3MB} compressbefore decompress GC CALL/EXT MEM= 3.3MBdecompress {after inflate... GC CALL/EXT MEM= 2003.4MB after promise. GC CALL/EXT MEM= 2003.4MB free output... GC CALL/EXT MEM= 1003.4MB} decompressafter decompress GC CALL/EXT MEM= 1003.4MBbefore decompress GC CALL/EXT MEM= 1003.4MBdecompress {after inflate... GC CALL/EXT MEM= 2003.4MB after promise. GC CALL/EXT MEM= 2003.4MB free output... GC CALL/EXT MEM= 1003.4MB} decompressafter decompress GC CALL/EXT MEM= 1003.4MBbefore decompress GC CALL/EXT MEM= 1003.4MBdecompress {after inflate... GC CALL/EXT MEM= 2003.4MB after promise. GC CALL/EXT MEM= 2003.4MB free output... GC CALL/EXT MEM= 1003.4MB} decompressafter decompress GC CALL/EXT MEM= 1003.4MBcreate buffer {at exit handler. GC CALL/EXT MEM= 1003.4MBnode:internal/buffer:959 super(bufferOrLength, byteOffset, length); ^RangeError: Array buffer allocation failed at new ArrayBuffer (<anonymous>) at new Uint8Array (<anonymous>) at new FastBuffer (node:internal/buffer:959:5) at Function.alloc (node:buffer:371:10) at createFilledBuffer (/home/xxx/src/script.js:32:22) at createAndFreeBuffer (/home/xxx/src/script.js:39:13) at main (/home/xxx/src/script.js:26:3) at runMicrotasks (<anonymous>) at processTicksAndRejections (node:internal/process/task_queues:96:5)Additional information
This was a related issue I previously raised and closed myself because it wasn't a leak and I was misguided by understanding how zlib worked #44750
But even now understanding the peak memory use would be 2x the inflated buffer (the chunks must be unified in another buffer) it still doesn't explain why it is not released.
I thought to open a new issue because the original title would be misleading. It's not a memory leak per se as it doesn't accumulate, but it nevers releases those buffers that might be needed by other parts of the program.
Version
v16.17.1
Platform
Linux 5.15.0-52-generic #58~20.04.1-Ubuntu SMP Thu Oct 13 13:09:46 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
Subsystem
zlib
What steps will reproduce the bug?
Code snippet (run with
--expose-gc)How often does it reproduce? Is there a required condition?
Always
What is the expected behavior?
Memory should lower back to near zero after finishing each decompression routine/scope.
What do you see instead?
Memory never lowers back to "zero" until the exit handler, not allowing it's reuse for something else.
It's not a cumulative memory leak, but keeps it reserved forever.
In a system with 2.5 GB memory limit it would crash due to OOM, even if we could fit 2GB easily if that memory held by zlib was released. Try it:
(ulimit -Sd 2500000; node --expose-gc ./script.js )Script output unbounded
$ node --expose-gc script.jsScript output boudned to 2500 MB (in Ubuntu, YMMV)
( ulimit -Sd 2500000 ; node --expose-gc script.js )Additional information
This was a related issue I previously raised and closed myself because it wasn't a leak and I was misguided by understanding how zlib worked #44750
But even now understanding the peak memory use would be 2x the inflated buffer (the chunks must be unified in another buffer) it still doesn't explain why it is not released.
I thought to open a new issue because the original title would be misleading. It's not a memory leak per se as it doesn't accumulate, but it nevers releases those buffers that might be needed by other parts of the program.