4

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

10 milliseconds heap

Using an interval of 1 second

  • 1000ms: polling started
  • 10000ms: polling stopped
  • 13000ms: ran a manual GC

1 second heap

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:

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';

*/
  • Well you create a new Promise at each iteration. Maybe a good old setInterval might help here. And i would not call it a memory leak. The usage seems to be constant, that gc kicks in should not be a problem – Jonas Wilms Dec 27 '17 at 15:47
  • setInterval is relentless while setTimeout is not: http://reallifejs.com/brainchunks/repeated-events-timeout-or-interval/. – Christian Martinez Dec 27 '17 at 15:50
  • Make sure to clear time outs, the `setTimeout()` returns an id that should be cleared to prevent rogue loops. This can be done with `clearTimeout()` shown here: http://mdn.beonex.com/en/DOM/window.clearTimeout.html – Phillip Thomas Dec 27 '17 at 15:51
  • @tuchi or just try to inline the *promise creation to wrap a setTimeout* thing – Jonas Wilms Dec 27 '17 at 15:52
  • @phillip thats definetly not the issue here – Jonas Wilms Dec 27 '17 at 15:52
  • @JonasW. what do you mean with the inline promise? – Christian Martinez Dec 27 '17 at 16:03
  • @PhillipThomas: could you explain it a little bit? – Christian Martinez Dec 27 '17 at 16:03
  • Which page does the client want updated? Is your utility thingy you're building related to this page in some way? Same page? How much of the heap is occupied with the app, or are the stats shown for the polling functions only? Do the messages get saved anywhere after getting polled? – Shilly Dec 27 '17 at 16:03
  • Timers have a tendency to hold onto the memory allocated inside of them. When you create an interval / timer expect to be responsible for clearing them. If I were you I would make sure to always use `clearTimeout([id])` in your promise resolution / `then()`. – Phillip Thomas Dec 27 '17 at 16:20
  • @Shilly * The utility will be exported as a module that can be used by any component/page. At the end, It will be part of a bundle js. * The test was ran against a blank Chrome page, using a Snippet. * The messages will be probably stored somewhere. I'll probably use some sort of notifying action so another module could use the polled messages – Christian Martinez Dec 27 '17 at 16:31
  • I still cant see a memory leak in your code... – Jonas Wilms Dec 27 '17 at 16:33
  • @JonasW. I guess that my biggest concern is what would happen when these "actions" gets more complex. Imagine that I'm trying to feed some UI element with this "poll" data. What would happen after 5 minutes of poll requests? – Christian Martinez Dec 27 '17 at 16:47
  • Jonas is right, the expected behavior for these timers is for the garbage collector to remove them which it looks like it did fine in your screenshots. The build up, then steep drop off is typical. A more likely issue with these timers is letting a `setInterval()` run forever without a clear. – Phillip Thomas Dec 27 '17 at 16:50
  • I see, but why I'm still having some junk in the heap right before I manually run the GC? Check both screenshots at 11000ms. Is it smth related to the `new Promise` stuff I'm doing? Or is it something "expected" when running these type of tests(maybe browser stuff, I don't know)? – Christian Martinez Dec 27 '17 at 17:09
  • Yes thats normal. GC tries to keep the process running, so it just cleans up until it freed enough mem – Jonas Wilms Dec 27 '17 at 21:42

0 Answers0