Suppose we have a programming task that is expected to take a lot of CPU time. A specific example might be the creation and enumeration of free polyominoes (see OEIS A000105). Another, easier example would be to find prime numbers up to some limit which isn't especially small. We can write a routine to do the task, but it will not be very responsive while intensively using the CPU.
For example, let's ask how long it takes to find prime numbers. We will do this in steps.
Seems like that should be pretty straightforward, but it isn't! Let's look at some code along with the results.
bigN = 5000000;
for (end_value = bigN; end_value <= bigN*3; end_value = end_value + bigN) {
let primes = [2, 3];
const beginTime = performance.now();
n = 3;
while (n <= end_value) {
n = n + 2;
let isPrime = true;
// Check if n is prime
for (let i = 3; i <= Math.sqrt(n); ++i) {
if (n % i === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(n);
}
}
const endTime = performance.now();
document.write(`Elapsed time = ${(endTime - beginTime) / 1000} seconds<br>`);
document.write(`Last 10 primes are<br>`);
for (let i = primes.length - 10; i < primes.length; i++) {
document.write(`${primes[i]} `);
}
document.write(`<br><br>`);
console.log(endTime-beginTime);
}
Even though the DOM write statements and the console.log prints are inside the "for loop" on end_values, those device prints are not executed until all of the loops are completed. As a consequence, there is no interim output and we have to wait for about 20 seconds before we see anything. Here is the eventual output.
Elapsed time = 2.027700000047684 seconds Last 10 primes are 4999871 4999879 4999889 4999913 4999933 4999949 4999957 4999961 4999963 4999999 Elapsed time = 5.831799999952317 seconds Last 10 primes are 9999889 9999901 9999907 9999929 9999931 9999937 9999943 9999971 9999973 9999991 Elapsed time = 12.451100000023843 seconds Last 10 primes are 14999861 14999867 14999903 14999917 14999921 14999947 14999953 14999969 14999977 14999981Had we decided to look at 10 or more rounds, we would have probably given up and assumed that the machine had hung, when in fact it was working along but just not reporting any progress.
A web worker is a separate javascript in its own file with a name such as "worker.js". We use a web worker because javascript is single threaded and as we have just seen can get bogged down doing a task and then ignore other perhaps more important tasks. We can name our primary javascript "main.js". In the html file, we had to use <script src="main.js"></script> in order to link the files. We do not use that syntax for worker.js. Instead, in our main.js file we will create the worker as follows:
worker = new Worker("worker.js");
Of course it doesn't do much good to run a separate instance of javascript unless the two instances can communicate. To do the communication, we use the same syntax for both main to worker and worker to main.
//in the worker.js
self.addEventListener("message", function(e) {
// the passed-in data is available via e.data
}, false);
or we can also use
onmessage = function(e) {
// the passed-in data is available via e.data
};
In order to keep multiple worker's messages from being mixed up,
//in the main.js for each worker,
worker.addEventListener('message', workerMessageHandler);
function workerMessageHandler(event) {
const msg = event.data;
Here is an example message SEND from worker.js
postMessage({
type: 'progress',
current: n,
count: primes.length,
highest: primes.length > 0 ? primes[primes.length - 1] : 0,
progress: n / end_value
});
And now, that message being received in main,
if (msg.type === 'progress') {
lastProgress = msg;
document.getElementById('result').textContent = msg.highest;
console.log(
`Progress: ${(msg.progress * 100).toFixed(1)}%`,
`| Primes found: ${msg.count}`,
`| Current: ${msg.current}`,
`| Highest: ${msg.highest}`
);
} else if (msg.type === 'complete') {
primes = msg.primes;
console.log('Worker completed! Total primes found:', msg.count);
document.getElementById('result').textContent = msg.highest;
}
}
1: let end_value; 2: let primes = [2, 3]; 3: //if you don't want timed reports, make maxDurtion a big number. 4: let maxDuration = 5 * 1000; //seconds * 1000 = milliseconds 5: let isRunning = false; 6: const CHUNK = 1270608; //How many primes to find before reporting 7: let n = 3; 8: let startTime; 9: let resumeTime = performance.now();Read any incoming data from your boss (main.js). First, !isRunning is true whenever isRunning is false. Translate !isRunning as 'is not running'.
23: self.onmessage = (event) => {
24: if (!isRunning) {
25: end_value = Number(event.data);
26: isRunning = true;
27:
28: // Start reporting progress every 'maxDuration' ms
29: const reportInterval = setInterval(() => {
30: if (isRunning) {
31: sendProgress();
32: } else {
33: clearInterval(reportInterval);
34: }
35: }, maxDuration);
36:
37:
38: // Start processing
39: processChunk();
40: }
41: };
We need two small functions to provide breaks in the work code which is coming up.
12: // Check if we should yield control based on time elapsed
13: function shouldYield(startTime, maxDuration) {
14: return ((performance.now() - startTime) > maxDuration);
15: }
16:
17: //Yield control to allow other tasks (like reporting) to run
18: function yieldControl() {
19: return new Promise(resolve => setTimeout(resolve, 0));
20: }
So we have set an interval timer which triggers at maxDuration (lines 29-35). Let's suppose that maxDuration is 5 seconds. The timer operates asynchronously. Therefore, we move on. The next instruction is to begin the work (i.e. call processChunk().) Lines 29-35 startup the interval at exactly approximately "Time1". We can't know exactly when that is!!
Let's look at the async work function. Our while loop will not be interrupted by the setInterval. It just doesn't work that way. But it does recognize a true or false condition and we have one provided by shouldYield(startTime,maxDuration). The variable "startTime" is set after "Time1" and not changed inside the while loop. At 5 seconds the interval timer would (if it could) send a progress report. It can't because we are still in the while loop. We will exit the loop only when ((performance.now() - startTime) > maxDuration) and that will not happen at the first interrupt because startTime < "Time1", even though it was set after "Time1". In order to make this work as written and to catch the first setInterval interrupt, we have to add a few milliseconds to the test condition. Hence,
while (n <= end_value && !shouldYield(startTime+10, maxDuration))
055: // Process a chunk of numbers, then schedule the next chunk
056: async function processChunk() {
057: //const chunkSize = CHUNK; // Numbers to check before yielding
058: startTime = performance.now(); //ms
059:
060: while (n <= end_value && !shouldYield(startTime+10, maxDuration)) {
061: n = n + 2;
062: let isPrime = true;
063: // Check if n is prime
064: for (let i = 3; i <= Math.sqrt(n); i++) {
065: if (n % i === 0) {
066: isPrime = false;
067: break;
068: }
069: }
070:
071: if (isPrime) {
072: primes.push(n);
073: if (primes.length % CHUNK === 0) {
074: await yieldControl();
075: sendProgress();
076: }
077: }
078:
079: // Report after each CHUNK:: An alternative that reports(yields) after n numbers have been tested
080: // if (n % CHUNK === 1) { //assure that this condition can occur
081: // await yieldControl(); //isn't needed but keeps you from getting ahead of yourself.
082: // sendProgress();
083: // }
084: }
085:
086: // If not done, schedule next chunk
087: // But pause long enough to ensure control goes beyond the async function.
088: if (n <= end_value) {
089: setTimeout(processChunk, 10); //After the Timeout It calls itself to continue
090: } else {
091: // Final report
092: postMessage({
093: type: 'complete',
094: primes: primes,
095: count: primes.length,
096: highest: primes[primes.length - 1]
097: });
098: isRunning = false;
099: }
100: }