Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aborted() error spamming the console with vips.Image.arrayjoin() #92

Open
jlarmstrongiv opened this issue Mar 7, 2025 · 12 comments
Open

Comments

@jlarmstrongiv
Copy link

I have multiple images sized 80x80px, each loaded with vips.Image.newFromBuffer(webpUint8Array)

vips.Image.arrayjoin(vipsImages) works fine images with a total of 10 images, 800 × 80px

vips.Image.arrayjoin(vipsImages) logs errors on a larger workload with a total of 60 images, 4800 x 80px

The funny thing is, although slower and logging errors, the arrayjoin function does eventually complete successfully.

Is there a different or more optimized function I should be using? Is there a limit to the size or number of images? It feels like 4800 x 80px should be doable.

The logs aren’t very helpful:

Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
@jlarmstrongiv
Copy link
Author

jlarmstrongiv commented Mar 7, 2025

If I halve the image size to 40x40px, the number of error logs decreases significantly

Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()

@kleisauke
Copy link
Owner

Are you able to provide a complete, standalone code sample that allows someone else to reproduce this issue? I could not reproduce this using this playground link.

Is there a different or more optimized function I should be using? Is there a limit to the size or number of images? It feels like 4800 x 80px should be doable.

I think vips.Image.arrayjoin() is already the most efficient method in this case. However, wasm-vips has a fixed initial and maximum memory size of 1 GiB.

# note 1: `ALLOW_MEMORY_GROWTH` may run non-wasm code slowly. See: https://github.com/WebAssembly/design/issues/1271.
# note 2: Browsers appear to limit the maximum initial memory size to 1GB, set `INITIAL_MEMORY` accordingly.

-sINITIAL_MEMORY=1GB

Given this constraint, an image size of 4800 x 80 ought to be fine.

@jlarmstrongiv
Copy link
Author

jlarmstrongiv commented Mar 8, 2025

Oh, that’s interesting. I ran into it every time when testing the code within my project. I can’t reproduce with 40px in a clean script, but I can with 80px. I made a simple reproduction in Node.js (v22.14.0 npx tsx script.ts and ensure input/output dirs exist). Please try running it multiple times, as it really is hit or miss as to whether it aborts or not:

import Vips from "wasm-vips";
import fs from "fs/promises";

function bufferToUint8Array(buffer: Buffer): Uint8Array {
  return new Uint8Array(
    buffer.buffer,
    buffer.byteOffset,
    buffer.byteLength / Uint8Array.BYTES_PER_ELEMENT
  );
}

export type Cleanup = () => void;

let _cleanup: Cleanup;

function cleanup() {
  if (typeof _cleanup === "function") {
    _cleanup();
  }
}

const vips = await Vips({
  preRun(module) {
    // cleanup https://github.com/kleisauke/wasm-vips/issues/13#issuecomment-1073246828
    module.setAutoDeleteLater(true);
    module.setDelayFunction((fn: Cleanup) => {
      _cleanup = fn;
    });
  },
});

const length = 60;

let vipsImages: Vips.ArrayImage = [];

for (let index = 0; index < length; index++) {
  // rather than 60 different sample images, load the same one 60 times
  const webp = bufferToUint8Array(await fs.readFile("src/assets/80px.jpg"));
  const vipsImage = vips.Image.newFromBuffer(webp);
  vipsImages.push(vipsImage);
}

const image = vips.Image.arrayjoin(vipsImages);
const webp = image.writeToBuffer(`.webp`);
await fs.writeFile("src/generated/sprite.webp", webp);

cleanup();

And the attached image

40px
Image
80px
Image

@jlarmstrongiv
Copy link
Author

jlarmstrongiv commented Mar 8, 2025

Could it be an issue with the automatic memory management?

Most of my code is synchronous, so perhaps garbage collection doesn’t have opportunities to run. That may also explain why it occurs earlier in my project, since I do a couple operations with vips beforehand. I always call cleanup() after those operations though.

EDIT: Just for the fun of it, in my project, I decided to call cleanup() again after I’ve written the joined sprite to a file, and that does seem to help. Does cleanup need to wait until it’s out of the function scope or until vips has had time to set the cleanup function? Would calling setImmediate(), setTimeout(() => {}, 0), or Promise.then() also help?
EDIT EDIT: It looks like my project scripts aren’t guaranteed to crash, similar to the example script I posted above. I’m having trouble tracking why this occurred and how to make the error consistent, even though it occurs frequently.

└──➤ npx tsx src/nodejs/pipelines/sprite-reproduction.ts
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()

node:internal/modules/run_main:122
    triggerUncaughtException(
    ^
unwind
Aborted()
(Use `node --trace-uncaught ...` to show where the exception was thrown)

Node.js v22.14.0
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
Aborted()
npx tsx src/nodejs/pipelines/sprite-reproduction.ts  94.50s user 66.92s system 976% cpu 16.533 total

└──➤ npx tsx src/nodejs/pipelines/sprite-reproduction.ts
79068
npx tsx src/nodejs/pipelines/sprite-reproduction.ts  99.28s user 193.30s system 1282% cpu 22.815 total

It would be a bit painful to go back through and change all my wasm-vips code to call image.delete() every single time I make a new image instance.

With manual memory management, would I need to call delete() on every variable? Or just once? I don’t always chain them, as I conditionally apply transformations in some cases.

const imageVips = vips.Image.newFromBuffer(image);
const imageVipsResize = imageVipsTrimmed.resize(scale, {
    kernel: vips.Kernel.lanczos3,
});
const flattenedImage = imageVipsResize
    .flatten({
      background: [255, 255, 255],
    });
const jpeg = flattenedImage.writeToBuffer(`.jpeg`)

imageVips.delete();
// vs
imageVips.delete();
imageVipsResize.delete();
flattenedImage.delete();

EDIT: after going back and reading #13 (comment), it looks like avoiding chains is the best way to clean memory manually

@jlarmstrongiv
Copy link
Author

It seems manual memory management is also inconsistent and sometimes fails.

import Vips from "wasm-vips";
import fs from "fs/promises";

function bufferToUint8Array(buffer: Buffer): Uint8Array {
  return new Uint8Array(
    buffer.buffer,
    buffer.byteOffset,
    buffer.byteLength / Uint8Array.BYTES_PER_ELEMENT
  );
}

const vips = await Vips();

const length = 60;

let vipsImages: Vips.ArrayImage = [];

for (let index = 0; index < length; index++) {
  const webp = bufferToUint8Array(await fs.readFile("src/assets/80px.jpg"));
  const vipsImage = vips.Image.newFromBuffer(webp);
  vipsImages.push(vipsImage);
}

const image = vips.Image.arrayjoin(vipsImages);

const webp = image.writeToBuffer(`.webp`);
for (let index = 0; index < length; index++) {
  vipsImages[index]?.delete();
}
image.delete();
await fs.writeFile("src/generated/duck.sprite.webp", webp);
console.log(webp.byteLength);

@kleisauke
Copy link
Owner

I wasn't able to reproduce this on my AMD Ryzen 9 7900 workstation, but I did notice a possible thread oversubscription issue with that sample. Could you try this patch?:

--- a/script.ts
+++ b/script.ts
@@ -21,6 +21,10 @@ function cleanup() {
 
 const vips = await Vips({
   preRun(module) {
+    // Handy for debugging.
+    //module.ENV.VIPS_INFO = 1;
+    module.ENV.VIPS_LEAK = 1;
+
     // cleanup https://github.com/kleisauke/wasm-vips/issues/13#issuecomment-1073246828
     module.setAutoDeleteLater(true);
     module.setDelayFunction((fn: Cleanup) => {
@@ -36,7 +40,9 @@ let vipsImages: Vips.ArrayImage = [];
 for (let index = 0; index < length; index++) {
   // rather than 60 different sample images, load the same one 60 times
   const webp = bufferToUint8Array(await fs.readFile("src/assets/80px.jpg"));
-  const vipsImage = vips.Image.newFromBuffer(webp);
+  const vipsImage = vips.Image.newFromBuffer(webp, "", {
+    access: vips.Access.sequential // "sequential"
+  });
   vipsImages.push(vipsImage);
 }
 
@@ -46,3 +52,5 @@ await fs.writeFile("src/generated/sprite.webp", webp);
 
 cleanup();
 
+// In order to make the leak checker work.
+vips.shutdown();

When opening the images in sequential access mode with the VIPS_LEAK environment variable set, I see:

vips_threadset_free: peak of 9 threads
memory: high-water mark 10.00 MB

However, in random access mode, I see:

vips_threadset_free: peak of 74 threads
memory: high-water mark 5.78 MB

So, significantly more threads are required when sequential access isn't used. If random access mode is required, you can might also try to lower the concurrency to 1, i.e. by calling vips.concurrency(1) after initializing wasm-vips.

wasm-vips/lib/vips.d.ts

Lines 68 to 74 in 88df6ba

/**
* Gets or, when a parameter is provided, sets the number of worker threads libvips' should create to
* process each image.
* @param concurrency The number of worker threads.
* @return The number of worker threads libvips uses for image evaluation.
*/
function concurrency(concurrency?: number): void | number;

@kleisauke
Copy link
Owner

It would be a bit painful to go back through and change all my wasm-vips code to call image.delete() every single time I make a new image instance.

You can also use explicit resource management as of wasm-vips v0.0.12. I added a type definition for this in commit 92dbf92.

@jlarmstrongiv
Copy link
Author

I have good news and bad news! Unfortunately, I can no longer reproduce the example after my machine restarted. I’ve been waiting in anticipation of the error occurring again.

However, when I was attempting various workarounds before reporting, I can say with confidence that I did try vips.Access.sequential and that I still encountered crashes (though perhaps less frequently?). Also, how do I know which operations require random access and which do not? (say resizing, cropping, trimming, extracting, extending/embedding, flattening/adding alpha, making histograms, joining images, etc.? Or is it only useful for operations like thumbnailing and joining images?)

I did not try vips.concurrency(1). Reading the docs, it seems like vips sets this number to a reasonable value. I assume there would be serious performance implications to setting it to 1? Also, can it be set multiple times, say to 1 for this operation and back to the default value afterwards?

It’s great to see the using keyword support! I’ve been looking forward to that becoming a standard part of JavaScript. I’ll have to check how well supported it is, but having both delete() and using is fantastic! The updated docs in the README are helpful too.

@jlarmstrongiv
Copy link
Author

Oh, the type definitions for using weren’t added in v0.0.12? It looks like I may need to wait until v0.0.13.

@kleisauke
Copy link
Owner

kleisauke commented Mar 23, 2025

Are you able to provide the output of?:

$ npx envinfo --binaries --system
$ node -p "require('os').cpus().length"
$ node -p "require('os').availableParallelism()"

This will help me understand the concurrency settings under which this is reproducible. Also, were you able to reproduce this on v0.0.10? libvips' threadpool was slightly refactored in version 8.15.5, so it could be due to that.

The interesting thing is that this possible thread oversubscription issue doesn't occur in Python. For example, this program:

Details
#!/usr/bin/env python3

# Usage: VIPS_LEAK=1 ./vips.py

import pyvips
from pathlib import Path

import logging
logging.basicConfig(level=logging.INFO)

bytes = Path('80px.jpg').read_bytes()

images = []
for i in range(60):
    images.append(pyvips.Image.new_from_buffer(bytes, ''))

joined = pyvips.Image.arrayjoin(images)
joined.write_to_file('x.webp')

del images
del joined

pyvips.shutdown()

Outputs:

vips_threadset_free: peak of 14 threads
memory: high-water mark 5.78 MB

This makes me wonder whether this is also needed on Node.js:

// Enforce a fixed thread pool by default on web
ENV['VIPS_MAX_THREADS'] = {{{ PTHREAD_POOL_SIZE }}};
// We cannot safely spawn dedicated workers on the web. Therefore, to avoid any potential deadlocks, we reduce
// the concurrency to 1. For more details, see:
// https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread
ENV['VIPS_CONCURRENCY'] = 1;

Also, how do I know which operations require random access and which do not? (say resizing, cropping, trimming, extracting, extending/embedding, flattening/adding alpha, making histograms, joining images, etc.? Or is it only useful for operations like thumbnailing and joining images?)

See: libvips/libvips#3963.

In some cases, it's not always obvious. For example, vips embed is sequential unless you pass --extend mirror and many crop operations on a single image require random access.

I did not try vips.concurrency(1). Reading the docs, it seems like vips sets this number to a reasonable value. I assume there would be serious performance implications to setting it to 1?

Indeed, it's more or less a workaround, but it could be useful to check if threading is the culprit here.

Also, can it be set multiple times, say to 1 for this operation and back to the default value afterwards?

That might work, but it's probably only useful for operations that trigger a pixel loop. Most operations just adds a node to the computation graph since libvips is "lazy".

Oh, the type definitions for using weren’t added in v0.0.12?

The type definition was the only missing part in v0.0.12; otherwise it should be fully supported.

kleisauke added a commit to kleisauke/libvips that referenced this issue Mar 23, 2025
Track waiting threads explicitly instead of relying on GAsyncQueue's
internal counter. This ensures the queue doesn't fill up between the
time a thread is spawned and when it pops a task off the queue, where
this counter was previously updated.

This issue likely only affected Wasm, as thread spawning is usually
instant in other environments.

See: kleisauke/wasm-vips#92.
kleisauke added a commit to kleisauke/libvips that referenced this issue Mar 23, 2025
Track the number of threads that haven't reached their entry point
to prevent thread oversubscription, which could occur on Wasm.

This issue likely did not affect other environments, where thread
spawning is usually instant.

See: kleisauke/wasm-vips#92.
@kleisauke
Copy link
Owner

With (draft) PR libvips/libvips#4436, I now see this with random access mode:

vips_threadset_free: peak of 17 threads
memory: high-water mark 5.77 MB

So, that significantly reduced the number of threads. However, I'm unsure if this is the root cause, as you mentioned reproducing the issue with sequential access, which shouldn't be an issue with that sample (as far as I know).

@jlarmstrongiv
Copy link
Author

jlarmstrongiv commented Mar 23, 2025

This will help me understand the concurrency settings under which this is reproducible. Also, were you able to reproduce this on v0.0.10? libvips' threadpool was slightly refactored in version 8.15.5, so it could be due to that.

I was writing and testing the reproductions using v0.0.11, but haven’t reproduced them since (I’ll be working on that project more in the next couple of weeks, and will keep an eye out for it)

Are you able to provide the output of:

  System:
    OS: macOS 15.3.1
    CPU: (16) x64 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
    Memory: 16.36 GB / 64.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.14.0 - ~/.asdf/installs/nodejs/22.14.0/bin/node
    npm: 11.2.0 - ~/.asdf/installs/nodejs/22.14.0/bin/npm

# node -p "require('os').cpus().length"
16
# node -p "require('os').availableParallelism()"
16

This makes me wonder whether this is also needed on Node.js:

Hmm, I run wasm-vips in Node.js on the main thread (mainly for scripting). Restricting concurrency and threads to the web defaults would significantly reduce performance?

See: libvips/libvips#3963.

Thank you! Understanding more about the access mode is very helpful

With (draft) PR libvips/libvips#4436, I now see this with random access mode

Great to see! I think that’ll improve the wasm version regardless

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants