0

I have a project where I need to write a function to calculate several things sequentially and then write the results to a SQL DB. Unfortunately, I need to repeat this over 40,000 times. I use node.js and promises to accomplish this but the memory usage goes to almost 2GB a lot of times the program just dies after 10,000 or so calculations. I developed my own native promiseEach function which takes a array items sequantially and chains it with promises.

What Am I doing wrong here?:

function promiseEach(array,promiseFn){
    return new Promise(function(resolve,reject){
        try {
            var promiseArray = [];
            var json = { array: array, counter: 0 }
            for (var i = 0; i < array.length; i++) { 
                promiseArray.push(promiseFn) 
            }
            promiseArray.reduce(function(preFn,curFn,index,pArray){
                return  preFn
                .then( function(z){ return json.array[json.counter++] })
                .then(curFn)
            }, 
            Promise.resolve(json.array[json.counter]))
            .then(resolve,reject)

        }catch(err){
            console.log("promiseEach ERROR:");
            reject(err)
        }
    })
}
Phil
  • 157,677
  • 23
  • 242
  • 245
ucipass
  • 923
  • 1
  • 8
  • 21
  • 1
    40,000 is a lot isn't it. I would start looking into how you might reduce that number first – Matthew Lock Oct 27 '16 at 03:51
  • What if you specify `--max-old-space-size` to something near 500Mb? – zerkms Oct 27 '16 at 04:15
  • Perhaps not the issue, but watch out for the [explicit promise construction antipattern](http://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it). – JLRishe Oct 27 '16 at 04:34
  • 2
    For situations that involve mega-structures, Node.js has another approach called streams and events. Try to convert your logic into streams. I think an array containing 40000 var is not large but a program containing 40000 function is too much! – Nidhin David Oct 27 '16 at 04:43
  • Spawn some threads and look how far you can get. – Stefan Rein Oct 27 '16 at 04:47
  • I think my initial approach was incorrect using an array of functions instead of just using the same function on an array of JSONs (as suggested by Jaromanda X). I would like to know more about using streams (mentioned by Nidhin David ) for the same problem as I am somehow not sure about chaining so many promises together is the best solution for this problem. – ucipass Oct 27 '16 at 14:50

2 Answers2

6

Well, you're overcomplicating the function for a start - from what I can tell, you're calling promiseFn for each of the content of array, in sequence

The following code is identical in function to your code

function promiseEach(array, promiseFn) {
    return array.reduce(function(prev, item) {
        return prev.then(function() {
            return promiseFn(item);
        });
    }, Promise.resolve());
}

No guarantee that this will fix the actual issue though

for completeness, as you are coding in nodejs, the same code as written in ES2015+

let promiseEach = (array, promiseFn) => 
    array.reduce((prev, item) => 
        prev.then(() => 
            promiseFn(item)
        )
    , Promise.resolve());
Jaromanda X
  • 53,868
  • 5
  • 73
  • 87
  • Your code is much simpler indeed and I really appreciate the rewrite!!! I will run my program if it makes a difference, but a the bigger question is if I have to run the same function a million times, would chaining 1 million promises is the best way to execute sequential async functions? – ucipass Oct 27 '16 at 14:44
  • 1
    I don't think this code will fix the issue, I think Traktor53 is on the right track to be honest – Jaromanda X Oct 27 '16 at 21:54
3

Two lines in the posted code

    .then( function(z){ return json.array[json.counter++] })
    then(curFn)

seem to indicate promseFn is to be called with a new parameter after the operation performed by a previous call to it completes, and that the fulfilled value of the previous call is not made use of in the promise chain.

A suggestion to avoid creating (39,999 or so) intermediate chained promises before returning from the each function, is to use intermediate promises returned by promiseFn to call a promise factory function when they become fulfilled, and return a final promise which is only fulfilled by the last intermediate promise.

The concept code which follows does not contain a call to reduce because no reduce operation is performed:

function promiseEach( array, promiseFn) {
    var resolve, reject;
    var counter = 0;
    var final = new Promise( function( r, j) { resolve = r; reject = j});
    function nextPromise() {
        promiseFn( array[counter++])
        .then( (counter < array.length ? nextPromise : resolve), reject);
    }
    if( array.length) {
        nextPromise();
    }
    else {
        reject( new Error("promiseEach called on empty array"));
    }
    return final;
}

Added Notes:

The promiseEach function above was tested in node/js using both a synchronous and asynchronous promiseFn with no concurrent updates to the data array.

  • Synchronous Test

    function promiseSynch( z) {
        return new Promise( function(resolve,reject){resolve(z);});
    }
    

was able to process an array with 1 million (1000,000) entries) in about 10 seconds using a Win7 i86 notebook with 1GB memory and a browser, editor and task manager open at the same time. Memory usage was flat at around 80% of available memory.

  • Asynchronous Test

    function promiseAsynch( z) {
        var resolve;
        function delayResolve() {
            resolve( z);
        }
        return new Promise( ( r, j)=>{
            resolve = r;
            setTimeout(delayResolve,1);
        });
    }
    

managed an array of 100,000 entries in a little over 20 minutes on the same notebook, again with flat memory usage of around 80%.

Conclusion

The testing suggests that Promises are not causing memory leaks simply because they are being used, and that they should be capable of handling passing lengthy array data sequentially into a promise returning function.

This implies there is some other causes of memory allocation failure, or mysterious processing outages, which needs to be found before the problem can be fully solved. As a suggestion you might start with a search for memory leak issues related to the DB library being used.

traktor
  • 17,588
  • 4
  • 32
  • 53
  • Are you aware of a limit related to recursive calls like the one suggested? I actually started out with a solution like you proposed, but got an error something like "too much recursion" after the size of the array got to a certain level. That's why I switched to the array#reduce call which did not seem to have this problem. I will try to run some additional tests. – ucipass Oct 27 '16 at 14:32
  • There is no recursion involved in calling `nextPromise` above. Promise reaction handlers are called asynchronously, with a clean stack, in a fresh thread after the promise `then` was called on becomes settled. – traktor Oct 27 '16 at 21:11
  • My program just finished running with your function and just when it got to around 39000 of 41000 count, I got a core dumped message with: FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory Aborted (core dumped) I will review my data allocation as I keep adding and deleting attributes to the JSON members of the above referenced array. – ucipass Oct 28 '16 at 14:54
  • Traktor, thank you for your comprehensive response. I did not want to post a complicated promiseFn function but each iteration of my function is generating roughly 100K worth of temporary JSON data which I am now deleting the JSON object (delete) before I resolve the function. You are right that there might be a memory leak somewhere else. I am not sure. I am going to accept your answer in the meantime and will post if I find the resolution. – ucipass Nov 08 '16 at 12:19