2

I'd like to run a task on a Meteor client which is resource hungry in the background and keep the interface responsive for the user in the meantime. The task does some math (for example finding prime numbers like described here: https://stackoverflow.com/a/22930538/2543628 ).

I've tried to follow the tips from https://stackoverflow.com/a/21351966 but still the interface always "freezes" until the task is complete.

setTimeout, setInterval and those packages like in my current approach also didn't help:

var taskQueue = new PowerQueue();
taskQueue.add(function(done) {
    doSomeMath();
    // It's still blocking/freezing the interface here until done() is reached
    done();
});

Can I do something to make the interface responsive during doSomeMath() is running or am I doing something wrong (also it doesn't look like there is much you could do wrong in PowerQueue)?

Community
  • 1
  • 1
Jey DWork
  • 1,746
  • 2
  • 16
  • 29

1 Answers1

4

JavaScript libraries which solve the problem of asynchronous queuing, assume that the tasks being queued are running in a concurrent but single-threaded environment like node.js or your browser. However, in your case you need more than just concurrency - you need multi-threaded execution in order to move your CPU-intensive computation out of your UI thread. This can be achieved with web workers. Note that web workers are only supported in modern browsers, so keep reading if you don't care about IE9.

The above article should be enough to get you started, however it's worth mentioning that the worker script will need to be kept outside of your application tree so it doesn't get bundled. An easy way to do this is to put it inside of the public directory.

Here is a quick example where my worker computes a Fibonacci sequence (inefficiently):

public/fib.js

var fib = function(n) {
  if (n < 2) {
    return 1;
  } else {
    return fib(n - 2) + fib(n - 1);
  }
};

self.addEventListener('message', (function(e) {
  var n = e.data;
  var result = fib(n);
  self.postMessage(result);
  self.close();
}), false);

client/app.js

Meteor.startup(function () {
  var worker = new Worker('/fib.js');
  worker.postMessage(40);
  worker.addEventListener('message', function(e) {
    console.log(e.data);
  }, false);
});

When the client starts, it loads the worker and asks it to compute the 40th number in the sequence. This takes a few seconds to complete but your UI should remain responsive. After the value is returned, it should print 165580141 to the console.

David Weldon
  • 63,632
  • 11
  • 148
  • 146
  • This sounds like the right direction. However, how would I access my libs/classes from meteor within the worker? importScripts() seems not very useful as meteor (especially in production mode)merges all files together giving it some temporary name... And I really would like to avoid double defining it as there are quite a lot of mathematical functions as well as a lib for big integers. The best thing I can think of right now is sym linking those files in /public/ and still use importScripts(). But I haven't tried it yet and would prefer a more elegant solution if there is any... – Jey DWork Apr 29 '14 at 21:17
  • The 'trick' with sym links in /public/ seems to work. Before importing the scripts `window = self` needs to be set in the worker (looks like my big integer lib needs this). But if there should be a better solution please let me know. – Jey DWork Apr 29 '14 at 21:41
  • It's *possible* that you can improve things by creating your own package, but I don't see a clean and obvious way to do it. I tried to research packages which use web workers and found [pdf.js](https://github.com/peerlibrary/meteor-pdf.js) - you may find something helpful there. If you need help creating a local package you can read my [blog post](https://dweldon.silvrback.com/local-packages). Finally, I'll warn you that the package system is going to be overhauled for 1.0 so you may want to stick to the symlink solution until that gets released. – David Weldon Apr 29 '14 at 21:53