Lance Pollard
Lance Pollard

Reputation: 79268

How to have a processor intensive function update every tick of the metronome and draw to canvas using web workers?

I have a cellular automaton which we can abbreviate like this:

// pretend slow update function called every tick of the metronome
const update = () => {
  let i = 0
  while (i < 10000) {
    console.log(i)
    i++
  }
}

Then I have in the main thread a Tone.js "loop" running every tick of the metronome:

new Tone.Loop(time => {
  // update() in worker
  // then draw() in main thread
}, '4n').start(0)

This loop essentially runs every quarter note at 240 BPM. You can approximate it with setTimeout, but with some extra fancy logic around keeping track of the elapsed time.

My question is, what is the architecture to make sure this draws every tick of the beat and doesn't get out of sync?

If I do this in the web worker system, then I am not sure how it will behave:

// main.js
worker.onmessage = () => draw()
new Tone.Loop(time => worker.postMessage('update'), '4n').start(0)

// worker.js
worker.onmessage = () => {
  update()
  postMessage("draw")
}

Depending on the async nature of how long the postMessage takes in both directions, the draw will come way after the beat potentially.

How do I do this correctly, architecture wise?

Note, the drawing to canvas must happen in the main thread, while the update function of the (multiple instances of) cellular automata must be updated all at once in the worker, for performance. Then there will be a SharedArrayBuffer to read the final computed values in the main.js.

What is the general approach I should take to wire this up?

Upvotes: 3

Views: 179

Answers (1)

Guerric P
Guerric P

Reputation: 31815

If I understand correctly you want to prevent frames from intertwining, preserve their order and do not delay them when they're ready, which means you need to skip those who don't respect the order.

You could pass the time back and forth, then use it to conditionally draw the frame.

// main.js
let lastFrameTime

worker.onmessage = ({ data: { time }}) => {
  if(time > lastFrameTime) {
    draw()
    lastFrameTime = time
  }
}
new Tone.Loop(time => worker.postMessage({ action: 'update', time }), '4n').start(0)

// worker.js
worker.onmessage = ({ data: { time }}) => {
  update()
  postMessage({ action: 'draw', time })
}

Edit: You actually need multiple workers per animation

If update takes more than 0.25s, then a callback will be queued in the worker before the previous one finishes, which will delay the next update call, and so on.

So you actually need multiple workers with some kind of round-robin dispatch between them, and use the code above in order to prevent frame intertwining.

Upvotes: 1

Related Questions