Search code examples
javascriptsqlitefirefox-addonfirefox-addon-restartlessfirefox-addon-bootstrap

Is using a while loop a good waiting strategy in a Firefox Restartless Extension?


I have a bootstrapped extension which interacts with the chrome part of Firefox (i.e. even before the content loads), and needs to query an SQLite database for some check. I would prefer a sync call. But, since a sync call is bad in terms of performance and can cause possible UI issues, I need to make an async DB call.

My use case is such:

  • Make aysnc call to database
  • Once completed do further processing

Now, this can be easily handled by placing 'further processing' part in handleCompletion part of executeAsync function.

But, I want the 'further processing' to be done irrespective of this statement being executed i.e. This DB lookup may or may not happen. If it doesn't happen well and good, go ahead. If it does I need to wait. So, I am using a flag based strategy; I set a flag handleCompletionCalled in handleError & handleCompletion callback to true.

In the further processing part, I do a

while(handleCompletionCalled) {
 // do nothing
}

//further processing

Is this a good strategy or can I do something better ( I don't really want to use Observers, etc. for this since I have many such cases in my entire extension and my code will be filled with Observers)?


Solution

  • Using a while loop to wait is a seriously Bad Idea™. If you do, the result will be that you hang the UI, or, at a minimum, drive CPU usage through the roof by rapidly running though your loop a large number of times as fast as possible.1

    The point about asynchronous programming is that you start an action and then another function, a callback, is executed once the activity is completed, or fails. This either allows you to start multiple actions, or to relinquish processing to some other part of the overall code. In general, this callback should handle all activity that is dependent on the completion of the asynchronous action. The callback function, itself, does not have to include the code to do the other processing. After it has done what needs to happen in response to the async action completing, it can just call another function like doOtherProcessing().

    If you launch multiple asynchronous, actions you can then wait for the completion of all of them by having flags for each task and a single function that is called at the end of all the different callback functions like:

    function continueAfterAllDone(){
        if(task1Done && task2Done && task3Done && task4Done) {
            //do more processing
        }else{
            //Not done with everything, yet.
            return;
        }
    }
    

    This could be extended to an arbitrary number of tasks by using an array, or task queue, which the function then checks to see if all of those are completed rather than a hard-coded set of tasks.

    Waiting:
    If you are going to have another processing path which executes, but then must wait for the completion of the asynchronous action(s), you should have the wait performed by setting up a timer, or interval. You then yield the processor for a specified period of time until you check again to see if the conditions you need to proceed have occurred.

    In a bootstrapped add-on, you will probably need to use the nsITimer interface to implement a timeout or interval timer. This is needed because at the time you are running your initialization code it is possible that no <window> exists (i.e. there may be no possibility to have access to a window.setTimeout()).

    If you are going to implement a wait for some other task, you could do it something like:

    const Cc = Components.classes;
    const Ci = Components.interfaces;
    
    var asyncTaskIsDone = false;
    var otherProcessingDone = false;
    // Define the timer here in case we want to cancel it somewhere else.
    var taskTimeoutTimer;
    
    function doStuffSpecificToResultsOfAsyncAction(){
        //Do the other things specific to the Async action callback.
        asyncTaskIsDone = true;
        //Can either call doStuffAfterOtherTaskCompletesOrInterval() here, 
        //  or wait for the timer to fire.
        doStuffAfterBothAsyncAndOtherTaskCompletesOrInterval();
    }
    
    function doStuffAfterBothAsyncAndOtherTaskCompletesOrInterval(){
        if(asyncTaskIsDone && otherProcessingDone){
            if(typeof taskTimeoutTimer.cancel === "function") {
                taskTimeoutTimer.cancel();
            }
            //The task is done
        }else{
            //Tasks not done.
            if(taskTimeoutTimer){
                //The timer expired. Choose to either continue without one of the tasks
                //  being done, or set the timer again.
            }
            //}else{ //Use else if you don't want to keep waiting.
            taskTimeoutTimer = setTimer(doStuffAfterBothAsyncAndOtherTaskCompletesOrInterval
                                        ,5000,false)
            //}
        }
    }
    
    function setTimer(callback,delay,isInterval){
        //Set up the timeout (.TYPE_ONE_SHOT) or interval (.TYPE_REPEATING_SLACK).
        let type = Ci.nsITimer.TYPE_ONE_SHOT
        if(isInterval){
            type = Ci.nsITimer.TYPE_REPEATING_SLACK
        }
        let timerCallback = {
            notify: function notify() { 
                callback();
            }
        }
        var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
        timer.initWithCallback(timerCallback,delay,type);
        return timer;
    }
    
    function main(){
       //Launch whatever the asynchronous action is that you are doing.
       //The callback for that action is doStuffSpecificToResultsOfAsyncAction().
    
        //Do 'other processing' which can be done without results from async task here.
    
        otherProcessingDone = true;
        doStuffAfterBothAsyncAndOtherTaskCompletesOrInterval();
    }
    

    Initialization code at Firefox startup:
    The above code is modified from what I use for delaying some startup actions which do not have to be done prior to the Firefox UI being displayed.

    In one of my add-ons, I have a reasonable amount of processing which should be done, but which is not absolutely necessary for the Firefox UI to be shown to the user. [See "Performance best practices in extensions".] Thus, in order to not delay the UI, I use a timer and a callback which is executed 5 seconds after Firefox has started. This allows the Firefox UI to feel more responsive to the user. The code for that is:

    const Cc = Components.classes;
    const Ci = Components.interfaces;
    
    // Define the timer here in case we want to cancel it somewhere else.
    var startupLaterTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    
    function startupLater(){
      //Tasks that should be done at startup, but which do not _NEED_ to be
      //  done prior to the Firefox UI being shown to the user.
    }
    
    function mainStartup(){
       let timerCallback = {
            notify: function notify() { 
                startupLater();
            }
        }
        startupLaterTimer = startupLaterTimer.initWithCallback(timerCallback,5000
                                                               ,Ci.nsITimer.TYPE_ONE_SHOT);
    }
    

    Note that what is done in startupLater() does not, necessarily, include everything that is needed prior to the ad-on being activated by the user for the first time. In my case, it is everything which must be done prior to the user pressing the add-on's UI button, or invoking it via the context menu. The timeout could/should be longer (e.g. 10s), but is 5s so I don't have to wait so long for testing while in development. Note that there are also one-time/startup tasks that can/should be done only after the user has pressed the add-on's UI button.

    1. A general programming issue here: In some programming languages, if you never yield the processor from your main code, your callback may never be called. In such case, you will just lock-up in the while loop and never exit.