“This article has participated in the call for good writing activities, click to view: the back end, the big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!”

In real-world scenarios, it is often possible to have Promise objects that are neither resolve nor reject. For example, the HTTP request is canceled. As we know, JavaScript memory management is based on reference counting, and there is no explicit way to tell promises that “you won’t use them anymore,” so in theory a large number of such promises could lead to memory leaks. But is this the case?

test

In a NodeJS 14.x environment, we tested the memory footprint of Promise.

Promise with no internal callback

Let’s look directly at what happens to the Heap memory after we create a billion Promise objects that are neither resolve nor Reject.

The test script

let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log('Memory occupied when the program starts:The ${Math.round(used * 100) / 100} MB`);

global.gc();
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log('GC takes up memory after startup:The ${Math.round(used * 100) / 100} MB`);

for (let i = 0; i < 1000000000; ++i) {
    new Promise(rs= > {
        if (Math.random() === NaN) {  // Construct an impossible condition
            rs();   // Never execute here, just to reference rs()
        }
    }).then(() = > {
        // This is not possible
        console.log('never resolved')})}; used = process.memoryUsage().heapUsed /1024 / 1024;
console.log('Promise will occupy memory after creation:The ${Math.round(used * 100) / 100} MB`);

global.gc();
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log('occupies memory after GCThe ${Math.round(used * 100) / 100} MB`);
Copy the code

The results

Memory used when the program is started: 1.99 MB Memory used after GC: 1.78 MB Memory used after Promise creation: 2.34 MB Memory used after GC: 1.77 MBCopy the code

After creating a billion unreleased Promise objects, memory remained essentially unchanged. In the above example, since there is no callback waiting or asynchronous invocation inside the Promise function, it is possible that the JavaScript engine has been optimized to release the Promise automatically.

With this in mind, let’s test again using a scenario with an internal callback waiting.

Call back incomplete promises

The test script

let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log('Memory occupied when the program starts:The ${Math.round(used * 100) / 100} MB`);

global.gc();
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log('GC takes up memory after startup:The ${Math.round(used * 100) / 100} MB`);

let rand = Math.random();
let N = 0;
for (let i = 0; i < 1000000; ++i) {
    new Promise(rs= > {
        setTimeout(() = > {            
            if (rand === 999) {  // Construct an impossible condition
                rs();   // Never execute here, just to reference rs()}},86400000);   // Wait 24 hours to execute, it will not be completed
        ++N;
    }).then(() = > {
        console.log('never resolved')})};setTimeout(() = > {
    console.log(N);
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log('Promise will occupy memory after creation:The ${Math.round(used * 100) / 100} MB`);

    global.gc();
    used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log('occupies memory after GCThe ${Math.round(used * 100) / 100} MB`);
}, 10000); // Memory will be measured after 10 seconds, and the 24 hour callback will not be completed
Copy the code

The results

Memory used when the program starts: 1.99 MB Memory used after GC starts: 1.78 MB 1000000 Memory used after Promise creation: 522.05 MB Memory used after GC: 521.98 MBCopy the code

As you can see, promises with internal callbacks are memory hogs. And when the internal callback is not complete, the memory is continually suspended, even if the GC does not free it automatically. So what happens to this memory if the callback completes, but still neither resolve nor reject? Continue testing…

Call back the completed Promise

The test script

let used = process.memoryUsage().heapUsed / 1024 / 1024; Console. log(' program startup memory: ${math.round (used * 100) / 100} MB '); global.gc(); used = process.memoryUsage().heapUsed / 1024 / 1024; Console. log(' GC footprint after startup: ${math.round (used * 100) / 100} MB '); let rand = Math.random(); let N = 0; for (let i = 0; i < 1000000; ++i) { new Promise(rs => { setTimeout(() => { ++N; If (rand === 999) {// Construct an impossible condition rs(); }). Then (() => {console.log('never resolved')})}; setTimeout(() => { console.log(N); used = process.memoryUsage().heapUsed / 1024 / 1024; Console. log(' Promise memory: ${math.round (used * 100) / 100} MB '); global.gc(); used = process.memoryUsage().heapUsed / 1024 / 1024; Console. log(' memory occupied after GC ${math.round (used * 100) / 100} MB '); }, 10000); // Wait 10 milliseconds for the callback above, 10 seconds for the callback to completeCopy the code

The results

Memory used when the program starts: 1.99 MB Memory used after GC starts: 1.78 MB 1000000 Memory used after Promise creation: 522.57 MB Memory used after GC 1.8 MBCopy the code

As you can see, any Promise that has a callback inside it will consume memory. However, after the callback is completed, the reference count for this part of memory should be cleared to zero, so it will be freed automatically after GC.

conclusion

  1. Incomplete executionPromise(including internally pending callback incomplete)It takes up memory.
  2. Executed completedPromise(including internal waiting callbacks are also executed),Does not take up memory and can be freed by GC.
  3. Executed completedPromiseEven if it is not triggeredresolverejectCan also beGCAutomatically release.
  4. In summary, there is no need to worry about neitherresolveAlso don’trejectPromiseObject causes a memory leak.