Context
I am writing a nodejs application that involves some heavy calculations. The underlying algorithm involves a lot of iteration and recursion. A quick summary of my code might look like this:
// routes.js - express.js routes
router.get('/api/calculate', (req, res) => {
const calculator = new Calculator();
calculator.heavyCalculation();
console.log('About to send response');
res.send(calculator.records);
});
// Calculator.js
class Calculator {
constructor() {
this.records = [];
}
heavyCalculation() {
console.log('About to perform a heavy calculation');
const newRecord = this.anotherCalculationMethod();
this.records.push(newRecord);
if (records.length < max) {
this.heavyCalculation();
}
};
anotherCalculationMethod() {
// more logic and deeper function calls
// to methods on this and other class instances
};
};
In most cases I would expect the max value to never be more than 9,000 or 10,000, and in some cases, it may be far less. As I said, this is a calculation heavy application, and I am trying to find the right approach to performing the calculations in a way that will not crash the machine the app is running on.
First issue - Maximum call stack size exceeded error
Even though what you see above is not an infinite loop, it quickly hits the Maximum call stack size exceeded error (for obvious reasons).
Here is a codesandbox demonstrating that issue. Click the button to make the api call which runs the function. If max is low enough, we get the expected behavior - heavyCalculation runs recursively, populates Calculator.records, and then when the limit is hit, the full results gets sent back to the front end. The logs look like this:
About to perform a heavy calculation
About to perform a heavy calculation
About to perform a heavy calculation
// repeats max times
About to send response
But if you set the max variable to be high enough, you'll exceed the stack limit, and the app crashes.
Trying to solve the issue with process.nextTick or setImmediate
I read the article The Call Stack is Not an Infinite Resource — How to Avoid a Stack Overflow in JavaScript, and opted to try some methods there. I liked the idea of using process.nextTick, as it would help dicretize each step in the recursive loop of heavyCalculation. Adjusting the method, I tried this:
heavyCalculation() {
// ...
if (records.length < max) {
process.nexTick(() => {
this.heavyCalculation();
});
}
};
This solves the max call stack size exceeded issue, but creates another problem. Rather than run heavyCalculation recursively, and then waiting until that recursion hits the limit, it runs the function once, then sends the response to the front end with records containing only a single entry, then continues the recursion. The same occurs when wrapping the call in setImmediate, i.e. setImmediate(this.heavyCalculation()). The logs look like this:
About to perform a heavy calculation
About to send response
About to perform a heavy calculation
About to perform a heavy calculation
About to perform a heavy calculation
// repeats max - 1 times
Codesandbox demonstrating the issue
As before, click the button to make the api call which fires the function, and you can see what's happening in the codesandbox nodejs terminal.
How can I properly manage the event loop such that my recursive function does not create a stack overflow, but fires in the proper order? Can I leverage process.nextTick? How can I separate the call stack for each iteration of the recursive function, but still wait for the end of the loop before returning the response?
Bonus question:
What I've posted is obviously a simplified version of my app. In reality, anotherCalculationMethod is in and of itself a complicated function with its own deep call stack. It iterates over a series of arrays, which may also grow to be quite large (on the order of thousands). Say it iterates over Calculator.records, and Calculator.records grows to be several thousand items large. Within a single iteration of heavyCalculation, how can I apply the same flow logic that might solve the main question to avoid the same issue happening within a subroutine like anotherCalculationMethod?