dlemstra / magick-wasm

The WASM library for ImageMagick

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Creates a blocking web server

reggi opened this issue Β· comments

commented

Given this code implemented in a deno server, it creates a server request handler that blocks. Meaning if you open up CURL and make two successive requests, the second request doesn't start until the first one finishes.

Is there any way to prevent this? Or is it just not feasible to have it run within a web-server like this? I can imagine that the wasm bundle code can only handle one image in memory at a time, which is why it blocks?

import { reservedPath } from "./code/cloud_asset/cloud_api/utils/reserved_path/mod.ts";
import { ImageMagick, initialize } from "./code/utils/imagemagick/mod.ts";

const file = await Deno.readFile('./my-image.png')
await initialize()

Deno.serve((_req) => {
  console.log(`request made`)
  if (reservedPath(_req.url)) {
    return new Response("Not Found", { status: 404 });
  }
  const headers = new Headers();
  headers.set("Content-Type", "image/png");
  const i = ImageMagick.read(file, (v) => {
    return v.write(g => g)
  })
  console.log(`request served`)
  return new Response(i, { headers });
})

I've tried awaiting everything, I've tried wrapping this ImageMagick.read in a promise, running of them and not awaiting, the first still needs to finish before the second starts. The fundamental code is sync and it seems that there is no way around it. I'm assuming there's a reason for this, something WASM related?

I have no clue why this is blocking, The ImageMagick code has locks for coders that don't support multithreading but that should not be an issue here. Maybe the wasm code can only run in a single thread? Don't think I can help you much because I don't have deno experience.

commented

Another attempt with no luck

import { Gravity, ImageMagick, Point, initializeImageMagick } from "https://deno.land/x/imagemagick_deno@0.0.19/mod.ts";

const file = await Deno.readFile('./image.png')
await initializeImageMagick()

let streamController: any;

function startStream(controller: ReadableStreamDefaultController) {
  streamController = controller;
  // You can start your stream empty
}

function updateStream() {
  ImageMagick.read(file, (v) => {
    v.compositeGravity(v, Gravity.North, new Point(1000, 1000))
    v.write(g => {
      streamController.enqueue(g)
      streamController.close()
      console.log(`request served πŸ‘`)
    })
  })
}

function handler(_req: Request): Response {
  console.log(`request started βœ…`)
  const body = new ReadableStream({
    start: startStream
  });

  updateStream()

  return new Response(body, {
    headers: {
      "content-type": "image/png",
      "x-content-type-options": "nosniff",
    },
  });
}

Deno.serve(handler);
commented

@oscarotero do you have any thoughts on this? How is Lume currently using this in a non-blocking way?

commented

Also to clarify non-blocking, requests appear to always be queued, and are not concurrent.

What I'm seeing:

request started βœ…
request served πŸ‘
request started βœ…
request served πŸ‘

What I want:

request started βœ…
request started βœ…
request served πŸ‘
request served πŸ‘

Note browsers will queue request to the same domains, you need to use curl to test.

commented

more findings:

import { Gravity, ImageMagick, Point, initializeImageMagick } from "https://deno.land/x/imagemagick_deno@0.0.19/mod.ts";

const file = await Deno.readFile('./image.png')
await initializeImageMagick()

console.log('a')

ImageMagick.read(file, (v) => {
  v.compositeGravity(v, Gravity.North, new Point(1000, 1000))
  v.write(g => {
    console.log(`done 1`)
  })
})

console.log('b')

ImageMagick.read(file, (v) => {
  v.compositeGravity(v, Gravity.North, new Point(1000, 1000))
  v.write(g => {
    console.log(`done 2`)
  })
})

console.log('c')

a
done 1
b
done 2
c

commented

and this seems not to block

import { Gravity, ImageMagick, Point, initializeImageMagick } from "https://deno.land/x/imagemagick_deno@0.0.19/mod.ts";

const file = await Deno.readFile('./image.png')
await initializeImageMagick()

console.log('a')

ImageMagick.read(file, async (v) => {
  await Promise.resolve()
  v.compositeGravity(v, Gravity.North, new Point(1000, 1000))
  v.write(g => {
    console.log(`done 1`)
  })
})

console.log('b')

ImageMagick.read(file, async (v) => {
  await Promise.resolve()
  v.compositeGravity(v, Gravity.North, new Point(1000, 1000))
  v.write(g => {
    console.log(`done 2`)
  })
})

console.log('c')

a
b
c
done 1
done 2

commented

this seems to work as I'd expect

import { Gravity, ImageMagick, Point, initializeImageMagick } from "https://deno.land/x/imagemagick_deno@0.0.19/mod.ts";

const file = await Deno.readFile('./image.png')
await initializeImageMagick()

ImageMagick.read(file, async (v) => {
  console.log('start 1')
  await Promise.resolve()
  v.compositeGravity(v, Gravity.North, new Point(1000, 1000))
  v.write(g => {
    console.log(`done 1`)
  })
})

ImageMagick.read(file, async (v) => {
  console.log('start 2')
  await Promise.resolve()
  v.compositeGravity(v, Gravity.North, new Point(1000, 1000))
  v.write(g => {
    console.log(`done 2`)
  })
})

start 1
start 2
done 1
done 2

Can we conclude this is something in Deno.serve then and close the issue here?

@reggi The problem is JavaScript is single-threaded language, it means that only can run one thing at a time. Async functions and promises don't allow to run things concurrently, but they are added to the event loop to be executed later. If you measure the total time spend to run the code, no matter if you use promises or not, the time will be the same. Just place this code at the end of your script to measure the time and you will see how logs similar values, no matter how many promises you use:

addEventListener('unload', () => {
  console.log(performance.now());
});

The only way to run code in parallel in JavaScript is with Workers.

const myWorker1 = new Worker(import.meta.resolve("./worker.ts"), { type: "module" });
const myWorker2 = new Worker(import.meta.resolve("./worker.ts"), { type: "module" });
const myWorker3 = new Worker(import.meta.resolve("./worker.ts"), { type: "module" });

This code will open tree workers that are run at the same time.

commented

@oscarotero thanks for your comment. I implemented this worker solution in the comments here https://stackoverflow.com/questions/76791229/wasm-imagemagick-based-code-blocks-deno-server, unfortunately I was trying to use Deno Deploy for my project and it doesn't support worker. Opened up a ticket with Deno Deploy (denoland/deploy_feedback#458) to check and see if the blocking issue here will also be an issue using Deno Deploy as it's on the edge, and instances should be spun up and handled when needed but I have batching concerns, anyway thanks for your comment. Good to know we're on the same page.