9

How can I limit a function to only run 10 times per second, but continue execution when new "spots" are available? This means we'd call the function 10 times as soon as possible, and when 1 second has elapsed since any function call we can do another call.

This description may be confusing - but the answer will be the fastest way to complete X number of API calls, given a rate limit.

Example: Here is an example that loops through the alphabet to print each letter. How can we limit this to only printLetter 10 times per second? I still want to loop through all letters, just at the appropriate rate.

function printLetter(letter){
  console.log(letter);
}

var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "X", "Y", "Z"];

// How can I limit this to only run 10 times per second, still loop through every letter, and complete as fast as possible (i.e. not add a hard spacing of 100ms)?
alphabet.forEach(function(letter){
  printLetter(letter);
});

A good solution will not forcefully space out each call by 100ms. This makes the minimum run time 1second for 10 calls - when you could in fact do these (nearly) simultaneously and potentially complete in a fraction of a second.

E_net4
  • 27,810
  • 13
  • 101
  • 139
Don P
  • 60,113
  • 114
  • 300
  • 432
  • 4
    is 10s a hard limit? because otherwise you could leverage the use of the `setInterval` function – Lucas Pottersky Nov 26 '15 at 20:06
  • Yes, it can't run anymore than 10 times per second. – Don P Nov 26 '15 at 20:08
  • 1
    @AndersonGreen That duplicate target doesn't have any real usable solution. The top rated answer requires the use of a third party library. –  Nov 26 '15 at 20:10
  • @AndersonGreen also no updates to the question in 5 years, and it doesn't have a specific solution for rate-limiting and then continuing execution – Don P Nov 26 '15 at 20:11
  • 3
    What does "but continue execution when new "spots" are available" mean? – Andy Nov 26 '15 at 20:17
  • Andy - imagine that 10 runs are kicked off almost immediately, then 1 second after each run began, we'd have available rate to spend on another function run. – Don P Nov 26 '15 at 20:39
  • Your comments and question edits don't make any sense. I know that what you gave is just an example, not your real code, but if you look at all the answers, you will notice that they all use `setTimeout` as it is the only way in JS to control the timing. Try to explain what you're trying to achieve, maybe then we can help you better. – Racil Hilan Nov 26 '15 at 20:53
  • 1
    @RacilHilan Not *all* of the answers use that method, and that is not the only method to limit the rate of function calls, as can be seen in my answer below. –  Nov 26 '15 at 20:56
  • 1
    Thanks for the second edit, it really clears things up. – nathanhleung Nov 26 '15 at 21:02
  • It's a tough question to state :P – Don P Nov 26 '15 at 21:07
  • @TinyGiant You must be kidding, right? First, I said it is the only way to control the timing, not to limit the rate of function calls. If you know of any other method, please let me know. Of course `setInterval` and `setTimeout` are more or less the same thing. Secondly, your answer uses both of those methods, so it is not different from all the other answers in this regards. Now about controlling the function rate, that's another thing and your answer is cool as I commented. – Racil Hilan Nov 26 '15 at 21:14
  • Hmmm, this last edit *the answer will be the fastest way to complete X number of API calls, given a rate limit* (of max 10 calls per second) is much clearer than your *edit 2*. I hope Tiny Giant's answer will work for you, otherwise you will have to give us the real scenario/ code in your project :-) From this edit, it looks to me that you're trying to make sure that the API does not get unreasonable # of calls, so it stays responsive and not get overwhelmed. – Racil Hilan Nov 26 '15 at 21:41

7 Answers7

11

Most of the other proposed solutions here evenly space the function calls using an interval or recursive function with a timeout.

This interpretation of your question doesn't really do what I believe you're asking for, because it requires you to call the function at a set interval.

If you would like to limit how many times a function can be called regardless of the space between the function calls, you can use the following method.


Define a factory function to hold the current time, count and queue then return a function which checks the current time against the last recorded current time and the count then either executes the first item in the queue, or waits until the next second to try again.

Pass a callback function to the function created by the factory function. The callback function will be entered into a queue. The limit function executes the first 10 functions in the queue and then waits until this interval has finished to execute the next 10 functions until the queue is empty.

Return the limit function from the factory function.

var factory = function(){
    var time = 0, count = 0, difference = 0, queue = [];
    return function limit(func){
        if(func) queue.push(func);
        difference = 1000 - (window.performance.now() - time);
        if(difference <= 0) {
            time = window.performance.now();
            count = 0;
        }
        if(++count <= 10) (queue.shift())();
        else setTimeout(limit, difference);
    };
};

var limited = factory();
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");

// This is to show a separator when waiting.
var prevDate = window.performance.now(), difference;

// This ends up as 2600 function calls, 
// all executed in the order in which they were queued.
for(var i = 0; i < 100; ++i) {
    alphabet.forEach(function(letter) {
        limited(function(){
            /** This is to show a separator when waiting. **/
            difference = window.performance.now() - prevDate;
            prevDate   = window.performance.now();
            if(difference > 100) console.log('wait');
            /***********************************************/
            console.log(letter);
        });
    });
}
3

You have to do it a little different:

var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "X", "Y", "Z"];

function printLetter(letterId) {
    if (letterId < alphabet.length) { // avoid index out of bounds

        console.log(alphabet[letterId]);

        var nextId = letterId + 1
        if (nextId < alphabet.length) // if there is a next letter print it in 10 seconds
            setTimeout("printLetter(" + nextId + ")", 10000/*milliseconds*/);
    }
}

printLetter(0); // start at the first letter

Demo:

var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "X", "Y", "Z"];

function printLetter(letterId) {
  if (letterId < alphabet.length) { // avoid index out of bounds

    console.log(alphabet[letterId]);
    document.body.innerHTML += "<br />" + alphabet[letterId]; // for ***DEMO*** only

    var nextId = letterId + 1
    if (nextId < alphabet.length) // if there is a next letter print it in 10 seconds
      setTimeout("printLetter(" + nextId + ")", 100 /*milliseconds*/ ); // one second for ***DEMO*** only
  }
}

printLetter(0); // start at the first letter
CoderPi
  • 12,985
  • 4
  • 34
  • 62
1

Recursive version always looks cooler

// Print the first letter, wait, and do it again on a sub array until array == []
// All wrapped up in a self-invoking function

var alphabet = ...
var ms      = 100 // 10 letters per seconds

(function printSlowly( array, speed ){

    if( array.length == 0 ) return; 

    setTimeout(function(){
        console.log( array[0] );
        printSlowly( array.slice(1), speed );
    }, speed );

})( alphabet, ms);
Radioreve
  • 3,173
  • 3
  • 19
  • 32
  • Except your code is not recursive, it returns the execution to `setTimeout` caller, i.e. to a timer process, not to `printSlowly` (not my downvote though). – Teemu Nov 26 '15 at 20:28
  • It is indirectly recursive, so it is recursive. – Ben Aston Nov 26 '15 at 20:29
  • You also defined `ms` but instead pass in `500` to the function instead. – Andy Nov 26 '15 at 20:30
  • 2
    This is not making maximum use of the rate-limit. It should kick off 10 runs immediately (since users should still receive the response as soon as possible). The fastest this can possibly complete for 10 runs is at least 1 second. – Don P Nov 26 '15 at 20:31
0

You can use setTimeout with the value 100 (which is a 1000 milliseconds / 10) to limit the output to 10 times per second. Use a variable call to count the calls. If you want to call the same function in other places, remember to reset the counter call to 1, so you start fresh:

var call = 1;

function printLetter(letter){
  call++;
  var x = call * 100;
  //alert(x);
  setTimeout(function(){ 
    document.getElementById("test").innerHTML += letter;
  }, x);
}

var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "X", "Y", "Z"];

// How can I limit this to only run 10 times per second, but still loop through every letter?
alphabet.forEach(function(letter){
  printLetter(letter);
});
<div id="test"/>
Racil Hilan
  • 24,690
  • 13
  • 50
  • 55
  • There's no need to use `forEach` if you're using `setTimeout`. – Andy Nov 26 '15 at 20:38
  • @Andy There is a zillion way to do something. If you ask me to code it my way, it would be completely different, but I'm not doing it my way, I'm doing it the OP's way because it is his code and project not mine. He asked a specific question: how to limit the function to 10 times per second, so I modified the function to do what he asked. I did not touch anything else, because none of us knows what the other parts should be doing and how he will be using it. – Racil Hilan Nov 26 '15 at 20:42
  • 2
    Fine, but it's not efficient. Also, why are you incrementing the timeout value with each iteration? That doesn't make any sense: `var x = call * 100`. – Andy Nov 26 '15 at 20:49
  • @Andy Seriously? If you're giving advices, then your JS skills must be good, which means you must be able to understand my simple answer easily. Why don't you copy my answer to a fiddle, remove that line and see what happens? The OP is calling the function in a loop and the execution time is almost instantaneous, so you get 26 calls almost at the same time. Without that line, the `setTimeout` will be called 26 times with the same delay 100, so all letters will be printed almost at the same time 100ms after the loop. Is it clear now, my friend? – Racil Hilan Nov 26 '15 at 21:01
0

Here's a recursive version with a callback (Is that what you mean by "continue execution when new spots are available"?)

EDIT: It's even more abstracted now - if you want to see the original implementation (very specific) see http://jsfiddle.net/52wq9vLf/0/

var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "X", "Y", "Z"];

/**
 * @function printLetter
 * @param {Array} array The array to iterate over
 * @param {Function} iterateFunc The function called on each item
 * @param {Number} start The index of the array to start on
 * @param {Number} speed The time (in milliseconds) between each iteration
 * @param {Function} done The callback function to be run on finishing
 */

function slowArrayIterate(array, iterateFunc, start, speed, done) {
    // Native array functions use these three params, so just maintaining the standard
    iterateFunc(array[start], start, array);
    if (typeof array[start + 1] !== 'undefined') {
        setTimeout(function() {
            slowArrayIterate(array, iterateFunc, start + 1, speed, done);
        }, speed);
    } else {
        done();
    }
};

slowArrayIterate(alphabet, function(arrayItem) {
    document.getElementById("letters").innerHTML += arrayItem;
}, 0, 100, function() {
    // stuff to do when finished
    document.getElementById("letters").innerHTML += " - done!";
});

Here's a jsfiddle: http://jsfiddle.net/52wq9vLf/2/

nathanhleung
  • 506
  • 6
  • 14
0

This is the best I could come up with in the time I had.

Note that this will not run correctly anything under Firefox v43 due to a bug in its implementation of fat-arrow functions.

var MAX_RUNS_PER_WINDOW = 10;
var RUN_WINDOW = 1000;

function limit(fn) {
    var callQueue = [], 
      invokeTimes = Object.create(circularQueue), 
      waitId = null;
    
    function limited() {        
        callQueue.push(() => {
            invokeTimes.unshift(performance.now())
            fn.apply(this, arguments);
        });
                
        if (mayProceed()) {
            return dequeue();
        }
        
        if (waitId === null) {
            waitId = setTimeout(dequeue, timeToWait());
        }
    }

    limited.cancel = function() {
      clearTimeout(waitId);
    };

    return limited;
    
    function dequeue() {
        waitId = null ;
        clearTimeout(waitId);
        callQueue.shift()();
        
        if (mayProceed()) {
            return dequeue();
        }
        
        if (callQueue.length) {
            waitId = setTimeout(dequeue, timeToWait());
        }
    }
    
    function mayProceed() {
        return callQueue.length && (timeForMaxRuns() >= RUN_WINDOW);
    }
    
    function timeToWait() {        
        var ttw = RUN_WINDOW - timeForMaxRuns();
        return ttw < 0 ? 0 : ttw;
    }

    function timeForMaxRuns() {
        return (performance.now() - (invokeTimes[MAX_RUNS_PER_WINDOW - 1] || 0));
    }
}

var circularQueue = [];
var originalUnshift = circularQueue.unshift;

circularQueue.MAX_LENGTH = MAX_RUNS_PER_WINDOW;

circularQueue.unshift = function(element) {
    if (this.length === this.MAX_LENGTH) {
        this.pop();
    }
    return originalUnshift.call(this, element);
}

var printLetter = limit(function(letter) {
    document.write(letter);
});

['A', 'B', 'C', 'D', 'E', 'F', 'G', 
'H', 'I', 'J', 'K', 'L', 'M', 'N', 
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 
'V', 'X', 'Y', 'Z'].forEach(printLetter);
Ben Aston
  • 53,718
  • 65
  • 205
  • 331
0

None of these were really much use. I'm not a very good developer, and I'm whipping together a test and wrote my own little set of functions. I couldn't understand what the accepted answer was doing, so maybe this will help someone else.

Should be fairly readable.

var queue = [];
var queueInterval;
var queueCallsPerSecond = 5;

function addToQueue(callback, args) {
    //push this callback to the end of the line.
    queue.push({
        callback: callback,
        args: args
    });

    //if queueInterval isn't running, set it up to run
    if(!queueInterval){

        //first one happens right away
        var nextQueue = queue.shift(); 
        nextQueue.callback(...nextQueue.args);

        queueInterval = setInterval(function(){
            //if queue is empty clear the interval
            if(queue.length === 0) {
                clearInterval(queueInterval);
                return false;
            }

            //take one off, run it
            nextQueue = queue.shift(); 
            nextQueue.callback(...nextQueue.args);

        }, 1000 / queueCallsPerSecond);
    }
}


//implementation addToQueue(callback, arguments to send to the callback when it's time to go) - in this case I'm passing 'i' to an anonymous function.
for(var i = 0; i < 20; i++){
    addToQueue(
        function(num) {
            console.log(num);
        },
        [i]
    );
}

Imagine you have a tray on your desk that people put tasks in... an inbox. Tasks get added by your coworkers faster than you can execute them, so you need to come up with a plan. You always take from the bottom of the stack and when the inbox is empty you can stop looking for what's next. That's all it does.

Motionharvest
  • 407
  • 4
  • 10