I was wondering if someone could help me with this issue that I have...
Our client has an Legacy API which retrieves messages from users, and they want us to implement a polling mechanism for it that, based on an specific interval, updates the information within the page. Period. They want to have a reliable polling strategy so (as you may already know) I'm using setTimeout to pull that off.
TL;DR: Does anyone one of you knows how to pull out an efficient polling utility that doesn't leak memory?
I'm trying to pull off an utility that allows me to add certain actions to a "polling list" and run the "polling" right there.
For example, I have an "actions list" similar to this:
const actions = new Map();
actions.set('1', ('1', {
action: () => {
console.log('action being executed...');
return 'a value';
},
onFinished: (...param) => { console.log(param); },
name: 'name of the action',
id: '1'
}));
I'm using Map for both api convenience and lookup performance and I'm adding a fake action to it (some of the params might not be needed but they're there for testing purposes).
Regarding the polling, I've created a delay fn to handle the timeout as a Promise (just for readability sake. It shouldn't affect the call stack usage whatsoever):
function delay(timeout) {
return new Promise(function delay(resolve) {
setTimeout(resolve, timeout);
});
}
And the polling fn that I came up with looks like this:
async function do_stuff(id, interval) {
const action = actions.get(id);
// breakout condition
if (action.status === 'stop') {
return;
}
console.log('processing...');
const response = await action.action();
console.log('executing action onFinished...');
action.onFinished(action.name, response);
delay(interval).then(function callAgain() {
do_stuff(id, interval);
});
}
I've used async/await in here because my action.action() will be mostly async operations and I'm using .then after the delay because I want to use the browser's EventTable to handle my resolve functions instead of the browser's stack. Also, I'm using named functions for debugging purposes.
To run the polling function, I just do:
const actionId = '1';
const interval = 1000;
do_stuff(actionId, interval);
And to stop the poll of that particular action, I run:
actions.get(actionId).status = 'stop'; // not fancy but effective
So far so good.... not! This surely has a ton of issues, but the one that bothers me the most of the JS Heap usage. I ran a couple of tests using the Performance Tab from Chrome DevTools (Chrome version 64) and this is what I got:
Using an interval of 10 milliseconds - 1000ms: polling started - 10000ms: polling stopped - 13000ms: ran a manual GC
Using an interval of 1 second
- 1000ms: polling started
- 10000ms: polling stopped
- 13000ms: ran a manual GC
Does anyone know why is this behaving like this? Why the GC it's running more frequently when I decrease the interval? Is it a memory leak or a stack issue? Are there any tools I could use to keep investigating about this issue?
Thanks in advance!
Stuff that I've read:
- http://reallifejs.com/brainchunks/repeated-events-timeout-or-interval/ (why choosing setTimeout instead of setInterval)
- Building a promise chain recursively in javascript - memory considerations
- How do I stop memory leaks with recursive javascript promises?
- How to break out of AJAX polling done using setTimeout
- https://alexn.org/blog/2017/10/11/javascript-promise-leaks-memory.html
PS: I've putted the snippet right here in case anyone wants to give it a try.
const actions = new Map();
actions.set('1', ('1', {
action: () => {
console.log('action being executed...');
return 'a value';
},
onFinished: (...param) => { console.log(param); },
name: 'name of the action',
id: '1'
}));
function delay(timeout) {
return new Promise(function delay(resolve) {
setTimeout(resolve, timeout);
});
}
async function do_stuff(id, interval) {
const action = actions.get(id);
// breakout condition
if (action.status === 'stop') {
return;
}
console.log('processing...');
const response = await action.action();
console.log('executing action onFinished...');
action.onFinished(action.name, response);
delay(interval).then(function callAgain() {
do_stuff(id, interval);
});
}
/*
// one way to run it:
do_stuff('1', 1000);
// to stop it
actions.get('1').status = 'stop';
*/