Node worker
dienstbereit opened this issue · 5 comments
Does anyone have an example an example of how writeFrame() can be implemented with a node worker? I can't manage to adapt the web worker example to a node worker.
Here's an example of using a pool of workers, queuing up frames across all of them, and then waiting for it all to finish before finalizing the encoder. This moves the quantization and encoding off the main UI thread.
https://github.com/mattdesl/looom-tools/blob/a9f455eeab3e43af6775e903cfafe6e9568dea4d/site/components/record.js#L250
Here's the worker:
https://github.com/mattdesl/looom-tools/blob/a9f455eeab3e43af6775e903cfafe6e9568dea4d/site/components/gifworker.js
You can test it online here too, click the circle (record) button:
https://looom-tools.netlify.app
Here's an example of using a pool of workers, queuing up frames across all of them, and then waiting for it all to finish before finalizing the encoder. This moves the quantization and encoding off the main UI thread. https://github.com/mattdesl/looom-tools/blob/a9f455eeab3e43af6775e903cfafe6e9568dea4d/site/components/record.js#L250
Here's the worker: https://github.com/mattdesl/looom-tools/blob/a9f455eeab3e43af6775e903cfafe6e9568dea4d/site/components/gifworker.js
You can test it online here too, click the circle (record) button: https://looom-tools.netlify.app
Hi Matt, thanks for your example. But unfortunately this is also based on web-workers in a browser with addEventListener etc.. Unfortunately, this cannot be adapted 1:1 for node for me. Specifically, it's about this part:
for (let i = 0; i < 30; i++) {
const canvas = new Canvas(Number(400), Number(400));
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.rect(0, 0, 400, 400);
ctx.fillStyle = 'green';
ctx.fill();
ctx.closePath();
ctx.fillStyle = "white"
ctx.font = [ '120px', 'OpenSans' ].join(' ');
ctx.textAlign = 'center';
ctx.fillText( i.toString(), 100, 100);
ctx.fillText( i.toString(), 100, 250);
ctx.fillText( i.toString(), 100, 400);
const ctx_image = ctx.getImageData(0, 0, 400, 400)
const { data, width, height } = ctx_image
const palette = quantize(data, 256);
const index = applyPalette(data, palette);
const delay = 1000
// outsource this part to node worker
gif.writeFrame(index, width, height, { palette, delay })
}
gif.finish();
const output = gif.bytes();
I thought maybe someone had already realised this in node with workers.
Is there a specific issue with node workers? I believe it should be possible in the same or similar way as done in a browser.
I have tried to convert your code for node workers. But unfortunately I always quickly run into issues because the code is too complex for me. That's why I've tried a new approach, but I haven't been able to successfully complete it yet. I think the problem here is await Promise.all(). The workers run through quickly, but then Promise.all waits until all workers have been processed. Here I would need another solution so that after a worker result the creation of gif.stream.writeBytesView() can already be started.
const express = require("express")
const app = express()
const {GIFEncoder, quantize, applyPalette} = require("gifenc")
const {Worker, workerData} = require("worker_threads");
const totalFrames = 30;
function createWorker(frame) {
return new Promise(function (resolve, reject) {
const worker = new Worker("./worker.js", {
workerData: {
frame: frame
},
});
worker.on("message", (data) => {
resolve(data);
});
worker.on("error", (msg) => {
reject(`An error occurred: ${msg}`);
});
});
}
const port = process.env.PORT || 3000;
app.get('/worker', async (req, res) => {
const workerPromises = [];
for (let i = 0; i < totalFrames; i++) {
workerPromises.push( createWorker(i) );
}
await Promise.all(workerPromises).then( (result) => {
const gif = GIFEncoder({ auto: false });
gif.writeHeader();
for (let i = 0; i < result.length; i++) {
gif.stream.writeBytesView(result[i]['data']);
}
gif.finish();
res.end(gif.bytesView());
})
})
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
I'm one step further ;-)
If I call workerPromises.push loop in the app.get('/worker') request, then the loading time is 800ms, if I put the workerPromises.push loop outside app.get('/worker'), then the loading time is 20ms!!! But now the problem is that the workerPromises.push loop is not called again with the next app.get('/worker') request. Is there any way that workerPromises.push is called again with every request?
const express = require("express")
const app = express()
const {GIFEncoder, quantize, applyPalette} = require("gifenc")
const {Worker, workerData} = require("worker_threads")
const port = process.env.PORT || 3000;
const totalFrames = 30;
function createWorker(frame) {
return new Promise(function (resolve, reject) {
const worker = new Worker("./worker.js", {
workerData: {
frame: frame
},
});
worker.on("message", (data) => {
resolve(data);
});
worker.on("error", (msg) => {
reject(`An error occurred: ${msg}`);
});
})
}
// 20ms
const workerPromises = [];
for (let i = 0; i < totalFrames; i++) {
workerPromises.push( createWorker(i) )
}
app.get('/worker', async (req, res) => {
// 800ms
// const workerPromises = [];
// for (let i = 0; i < totalFrames; i++) {
// workerPromises.push( createWorker(i) )
// }
await Promise.all(workerPromises).then((result) => {
const gif = GIFEncoder({auto: false});
gif.writeHeader();
for (let i = 0; i < result.length; i++) {
gif.stream.writeBytesView(result[i]['data']);
}
gif.finish();
res.writeHead(200, {
'Content-Type': 'image/gif',
'Content-Length': gif.bytes().length,
'Cache-Control': 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0',
'Pragma': 'no-cache',
'Expires': '-1'
});
res.end(gif.bytesView());
})
})
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});