Pattern for interrupting heavy computation on brow

2019-06-09 10:24发布

问题:

I'm working on a client side simulation that does on the fly background computation and view refresh. However, because the simulation is always live, the CPU ends up doing a lot of unnecessary work during intense user inputs and edits.

What I want to achieve is a way to kill the whole sequence on user event.

anticipated usage in the main app:

var sequence =  new Sequence(heavyFunc1, heavyFunc2, updateDom);
document.addEventListener("click", sequence.stop)
sequence.run() //all the heavy computation runs until told to stop

anticipated usage in a web worker:

var sequence =  new Sequence(heavyFunc1, heavyFunc2, self.postmessage);
self.onmessage = function(e) {
    if (e.data === 'stop') msg = sequence.stop;
    else msg = sequence.run(e.data); //resets and restarts
};

I've looked around and can think of the following tools and patterns:

setTimout(fcn,0) || setImmediate(fcn) shim : Wrap the individual steps setTimeout(fcn,0) inside the sequences in both the main script and worker to processe new events before the end of the sequence.

killFlag = false;
window.addEventListener('keypress', function() {killFlag = true});

//this exampe works only with setTimeout, fails with setImmediate lib
var interruptibleSequence = function(tasks) {
  var iterate = function() {
    if (killFlag || !tasks.length) return;
    tasks.shift()();
    if (tasks.length) window.setTimeout(iterate,0);
  };
  iterate();
};

This example worked with setTimeout but failed with setImmediate where the keypress event always came in last.

debounce : This is the typical answer that does not seem to apply in my case. Delaying and batching user inputs would partly reduce the processing intensity at the expense of longer processing time.

Promises : I'm already using promises for worker results and could introduce additional promises between steps to interrupt the sequence and process new events. I've tried (and failed) using either flag checks or Promise.race.

With kill flag

killFlag = false;
window.addEventListener('keypress', function() {killFlag = true});

//does not work. promises get priority and event is only triggered last
var interruptibleSequence = function(tasks) {
  var seq = Promise.resolve();
    tasks.forEach(function(task){
      seq = seq.then(function() {
        if (killFlag) return;
        else task();
    });
  });
};

With Promise.race

var killTrigger;
var killPromise = new Promise(function(res,rej){killTrigger = rej});
window.addEventListener('keypress', killTrigger());

//does not work. promises get priority and event is only triggered last
var raceToFailure = function(tasks) { 
  var seq = Promise.resolve();
  tasks.forEach(function(task){
    seq = Promise.race([seq.then(task),killPromise]);
  });
};

Question What would be the recommended pattern to kill a sequence on event?

回答1:

In Short: Call Stack >> Promise >> Message Queue (events & setTimeout)

Turns out the question was badly formulated and too specific to a particular use case. In general, for sync functions that return immediately, wrapping them in a sequence of promise does hand over control to the next function but still take precedence over events in the message queue, even those fired before the Promises were created. The accepted answer does not always work.

In the snippet below, the Event, Promise, Sync calls end up being executed in the Sync, Promise, Event order. (native Promises required)

The only way therefore to let events interrupt a heavy computation is to stage the heavy sync functions with setTimeout.

//prep
function log(m) {document.getElementsByTagName('pre')[0].innerHTML += m+'<br>'}
function syncDelay(ms) {
    for (var tgt=Date.now()+ms; Date.now()<tgt;) Math.random();
}

function async1() {syncDelay(100); log('1. async1')}
window.addEventListener('message', function(e){log(e.data)});
function syncPr1() {syncDelay(100); log('3. syncPromise1')}
function syncPr2() {syncDelay(100); log('4. syncPromise2')}
function syncPr3() {syncDelay(100); log('5. syncPromise3')}


//sequence
setTimeout(async1,0);
window.postMessage('2. events get done last', '*');

Promise.resolve()
.then(syncPr1)
.then(syncPr2)
.then(syncPr3)
.catch(function(e){console.log(e)});

log('6. sync items get executed before next promise call');
<pre></pre>