Back to Primes

Operating buttons on the Primes page

This Page is supposed to be about Javascript Web Workers!
The example uses "Timing Prime Number Creation" A 'web worker' finds the primes and reports back to the html page. You, the user, need to pick between
  1. Letting it report back after a given number of seconds
  2. Letting it report back after every fixed number of primes found.
If you pick such that there is a conflict, then your reports may get tangled up.

Report back after a Fixed Number of Seconds

  1. Max Search number is the largest number to check for prime-ness before stopping.
  2. Report Interval(sec) is the number of seconds to search before pausing and making a report on the html screen.
  3. "Report after ___ primes" needs to be greater than the Max Search Number and there won't be any conflict because the search for primes will end before getting to the 'Report aften ____ primes'.

Report Back after finding 'x' primes.

  1. Max Search number is still the largest number to check for prime-ness before stopping.
  2. Since we don't want to be interruped by the clock, set the Report Interval(sec) to a very large number. For example 31556952, which is a years worth of seconds.
  3. Set the "Report after ___ primes" to be the number of primes found prior to reporting. For example, if you want to search thru the 1st 20e6 primes and show how long it took after each 100,000 primes are found, then the Max Search # = 20e6, The Report Interval(sec) = 31e6, and the "Report after" = 100e3.
Use the Start Worker button to begin. The "Show Primes" Button only shows the last 10 primes. This is really about timing.

If you are only here for the prime numbers, go back. You are done.

Web Workers

An Issue with Javascript

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.

  1. Elapsed Time to find primes up to 5E6.
  2. Elapsed Time to find primes up to 10E6
  3. Elapsed Time to find primes up to 15E6
We can write javascript code to do this. First let's just do an outline.

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 14999981
Had 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.

Web Workers

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.

Sending

For sending a message to either file uses postMessage. To send from main to worker, in the main we write:
worker.postMessage(data)    from main => worker
To send from the worker we write
postMessage(data)    from worker => main
"function postMessage(message: any, options?: WindowPostMessageOptions): void (+1 overload)"
Posts a message to the given window. Messages can be structured objects, e.g. nested objects and arrays, can contain JavaScript values (strings, numbers, Date objects, etc), and can contain certain data objects such as File Blob, FileList, and ArrayBuffer objects. he message parameter is mandatory. If the data to be passed to the worker is unimportant, null or undefined must be passed explicitly.
Objects listed in the transfer member of options are transferred, not just cloned, meaning that they are no longer usable on the sending side.
A target origin can be specified using the targetOrigin member of options. If not provided, it defaults to "/". This default restricts the message to same-origin targets only.
If the origin of the target window doesn't match the given target origin, the message is discarded, to avoid information leakage. To send the message to the target regardless of origin, set the target origin to "*".

Receiving

In this case we do not have to specify who is receiving because the main can receive a message from any sender and the worker can receive a message only from 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;
    }
}

worker.js Process Flow

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: }