Don P
Don P

Reputation: 63547

Rate limit a javascript function

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.

Upvotes: 9

Views: 10270

Answers (7)

Motionharvest
Motionharvest

Reputation: 447

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.

Upvotes: 0

Ben Aston
Ben Aston

Reputation: 55729

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

Upvotes: 0

user4639281
user4639281

Reputation:

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);
        });
    });
}

Upvotes: 11

nathanhleung
nathanhleung

Reputation: 516

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/

Upvotes: 0

Racil Hilan
Racil Hilan

Reputation: 25341

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"/>

Upvotes: 0

CoderPi
CoderPi

Reputation: 13211

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

Upvotes: 3

Radioreve
Radioreve

Reputation: 3309

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

Upvotes: 1

Related Questions