mattdesl / gifenc

fast GIF encoding
MIT License
276 stars 19 forks source link

Node worker #18

Open dienstbereit opened 1 week ago

dienstbereit commented 1 week ago

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.

mattdesl commented 1 week ago

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

dienstbereit commented 1 week ago

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.

mattdesl commented 1 week ago

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.

dienstbereit commented 1 week ago

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}`);
});
dienstbereit commented 1 week ago

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}`);
});