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

asynchronous drawImage speeds up execution but causes serious memory leaks. #890

Open
rambo-panda opened this issue Sep 9, 2024 · 21 comments · Fixed by #913 or #914
Open

asynchronous drawImage speeds up execution but causes serious memory leaks. #890

rambo-panda opened this issue Sep 9, 2024 · 21 comments · Fixed by #913 or #914
Assignees

Comments

@rambo-panda
Copy link
Contributor

// this is pmap status
0000000000400000    7644    3104       0 r---- node
0000000000b77000       8       8       0 r-x-- node
0000000000b7a000   27296   13892       0 r-x-- node
0000000002800000       4       0       0 r-x-- node
0000000002801000   46796    8980       0 r---- node
00000000055b4000      16      16       8 r---- node
00000000055b8000     124     108      92 rw--- node
00000000055d7000     180     168     168 rw---   [ anon ]
00000000075e0000 5697364 5697132 5697132 rw---   [ anon ]
00000266d94c0000     256     256     256 rw---   [ anon ]
0000033e93480000     256       8       8 rw---   [ anon ]
0000036b35cc0000     256     256     256 rw---   [ anon ]
0000039136800000     256     256     256 rw---   [ anon ]
000003f309900000     256     256     256 rw---   [ anon ]
000004103ff00000     256     256     256 rw---   [ anon ]

image
The memory does not automatically drop to normal levels, even though I have enabled GC and cleared all cache.

@rambo-panda
Copy link
Contributor Author

#867

@Horziox
Copy link

Horziox commented Sep 9, 2024

After several tests on my side and after proceeding by elimination, I confirm I have the same issue.
The buffers generated by drawImage cannot be cleared by Node's Garbage Collector, inevitably leading to a memory leak.

@Brooooooklyn Brooooooklyn self-assigned this Sep 10, 2024
@bingtsingw
Copy link

I have the same issue

@rambo-panda
Copy link
Contributor Author

canvas/src/image.rs

Lines 417 to 424 in 8ca3a9e

let this: This = env.get_reference_value(&self.this_ref)?;
let mut image_ptr = ptr::null_mut();
check_status!(
unsafe { sys::napi_unwrap(env.raw(), this.raw(), &mut image_ptr) },
"Failed to unwrap Image from this"
)?;
let self_mut = unsafe { Box::leak(Box::from_raw(image_ptr.cast::<Image>())) };
self_mut.width = output.width;

// 'static pointer
let self_mut = unsafe { Box::leak(Box::from_raw(image_ptr.cast::<Image>())) }; 

@rambo-panda
Copy link
Contributor Author

rambo-panda commented Sep 12, 2024

canvas/src/image.rs

Lines 417 to 424 in 8ca3a9e

let this: This = env.get_reference_value(&self.this_ref)?;
let mut image_ptr = ptr::null_mut();
check_status!(
unsafe { sys::napi_unwrap(env.raw(), this.raw(), &mut image_ptr) },
"Failed to unwrap Image from this"
)?;
let self_mut = unsafe { Box::leak(Box::from_raw(image_ptr.cast::<Image>())) };
self_mut.width = output.width;

// 'static pointer
let self_mut = unsafe { Box::leak(Box::from_raw(image_ptr.cast::<Image>())) }; 

@Brooooooklyn since I have limited understanding of @napi-rs, I am hesitant to change the lifecycle of self_mut rashly. Is it possible to make the following modification for the time being?

  #[napi(setter)]
  pub fn set_src(&mut self, env: Env, this: This, data: Uint8Array) -> Result<()> {
    let length = data.len();
    if length <= 2 {
      self.src = Some(data);
+     self.bitmap = None;
+     self.width = -1.0;
+      self.height = -1.0;

Of course, I have my own motives, as it just happens to meet another need of mine. #868

@rambo-panda

This comment was marked as resolved.

@rambo-panda

This comment was marked as resolved.

@bingtsingw
Copy link

@Brooooooklyn Hi, are there any progress on this issue, it's a serious problem on server, after a few requests, the server memory usage is full.

@rambo-panda
Copy link
Contributor Author

@Brooooooklyn I tested and found that the memory usage has not decreased for a long time. This is my test code.

import { clearAllCache, createCanvas, loadImage } from "@napi-rs/canvas";

const imgBuf = await fetch(
  "https://cdn-ms.17zuoye.cn/zx-ptqlm/testing_2023_10_27/pdf_6d1e8d355613ef824d_0.webp",
).then((a) => a.arrayBuffer());

const len = ~~(process.argv[2]??50);

console.time("drawImage");
const res = await Promise.all(
  Array(len)
    .fill(0)
    .map(async () => {
      // console.time(i);
      const img = await loadImage(imgBuf);
      const canvas = createCanvas(img.width, img.height);
      canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height);
      await canvas.encode("webp", 80);
      // console.timeEnd(i);
    }),
);
console.timeEnd("drawImage");

setInterval(() => {
  global.gc?.();
  clearAllCache();
  console.log(process.memoryUsage.rss() / 1024 / 1024);
  // decreasing array length
  res.splice(0, 10);
}, 1000);

image
image

@Horziox
Copy link

Horziox commented Oct 14, 2024

If I reuse the script from @rambo-panda , the fix doesn't seem to work
{10173544-F76D-4EF7-B4A2-DE65DCB9A889}

@Brooooooklyn
Copy link
Owner

If I reuse the script from @rambo-panda , the fix doesn't seem to work

Why do you think it doesn't work? The memoryUsage.rss() can not be 0

@Horziox
Copy link

Horziox commented Oct 15, 2024

If I reuse the script from @rambo-panda , the fix doesn't seem to work

Why do you think it doesn't work? The memoryUsage.rss() can not be 0.

Because in my script I generate GIFs (around 5 each time)
Each frame has about 10 to 20 loadImage() calls,
and each GIF has between 5 and 10 frames
When I run this script three times, my 8GB of RAM gets maxed out, and Ubuntu starts using swap, if the server doesn't crash

However, the script is written in TypeScript, so the promises are properly resolved
I reused Panda's script to better illustrate the issue

But in Chrome's inspector, when using the --inspect flag, you can clearly see that ArrayBuffer instances keep growing and are never purged
The only way to clean them up is to stop the script or wait for the server to crash
Alternatively with PM2, I can set a memory usage limit so that PM2 automatically restarts the process

@Horziox
Copy link

Horziox commented Oct 15, 2024

I had also modified Panda's script by logging the RSS by default before logging it at intervals
At the beginning, it reported 61-62MB of usage, which is normal
But after that, it stayed stuck at 90MB
Normally, after clearing the constant named res that holds all the loadImage instances, it should have gone back down to around 65MB, or maybe 70MB if some compiled code was added to the RSS during execution
However, that's not the case
The amount of RAM used by the ArrayBuffer generated by loadImage increases proportionally and exponentially each time the script is called

@bingtsingw
Copy link

I tested 0.1.58 on my server, the same result.
The memory did not released until my server got a maxMemoryUsage crash

@Brooooooklyn Brooooooklyn reopened this Oct 16, 2024
@Brooooooklyn
Copy link
Owner

@bingtsingw can't reproduce the memory leak with this:

import { whiteBright, red, green, gray } from 'colorette'
import prettyBytes from 'pretty-bytes'
import { table } from 'table'

import { clearAllCache, createCanvas, loadImage } from "./index.js";

const imgBuf = await fetch(
  "https://cdn-ms.17zuoye.cn/zx-ptqlm/testing_2023_10_27/pdf_6d1e8d355613ef824d_0.webp",
).then((a) => a.arrayBuffer());

const initialMemoryUsage = process.memoryUsage()

const len = ~~(process.argv[2] ?? 50);

console.time("drawImage");
const res = await Promise.all(
  Array(len)
    .fill(0)
    .map(async () => {
      // console.time(i);
      const img = await loadImage(imgBuf);
      const canvas = createCanvas(img.width, img.height);
      canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height);
      await canvas.encode("webp", 80);
      // console.timeEnd(i);
    }),
);
console.timeEnd("drawImage");

function displayMemoryUsageFromNode(initialMemoryUsage) {
  const finalMemoryUsage = process.memoryUsage()
  const titles = Object.keys(initialMemoryUsage).map((k) => whiteBright(k))
  const tableData = [titles]
  const diffColumn = []
  for (const [key, value] of Object.entries(initialMemoryUsage)) {
    const diff = finalMemoryUsage[key] - value
    const prettyDiff = prettyBytes(diff, { signed: true })
    if (diff > 0) {
      diffColumn.push(red(prettyDiff))
    } else if (diff < 0) {
      diffColumn.push(green(prettyDiff))
    } else {
      diffColumn.push(gray(prettyDiff))
    }
  }
  tableData.push(diffColumn)
  console.info(table(tableData))
}

setInterval(() => {
  global.gc?.();
  clearAllCache();
  displayMemoryUsageFromNode(initialMemoryUsage);
  // decreasing array length
  res.splice(0, 10);
}, 1000);

And the output:

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +53.3 MB │  0 B      │ +150 kB  │ +33.5 kB │  0 B         ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +54.3 MB │ +524 kB   │ -347 kB  │ +33.5 kB │ -492 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +54.7 MB │ +524 kB   │ +319 kB  │ +33.5 kB │ -492 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss    │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟────────┼───────────┼──────────┼──────────┼──────────────╢
║ +56 MB │ +1.05 MB  │ -576 kB  │ -492 kB  │ -492 kB      ║
╚════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +56.3 MB │ +1.31 MB  │ +111 kB  │ -492 kB  │ -492 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +56.3 MB │ +1.31 MB  │ +459 kB  │ -492 kB  │ -492 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +56.4 MB │ +1.31 MB  │ +810 kB  │ -492 kB  │ -492 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +56.4 MB │ +1.31 MB  │ +1.16 MB │ -492 kB  │ -492 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +51.3 MB │ -3.15 MB  │ -1.83 MB │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +51.4 MB │ -3.15 MB  │ -1.69 MB │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +51.8 MB │ -3.15 MB  │ -1.13 MB │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +52.2 MB │ -3.15 MB  │ -772 kB  │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +52.6 MB │ -2.1 MB   │ -1.41 MB │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss    │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟────────┼───────────┼──────────┼──────────┼──────────────╢
║ +53 MB │ -2.1 MB   │ -1.06 MB │ -500 kB  │ -497 kB      ║
╚════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +53.3 MB │ -2.1 MB   │ -713 kB  │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +52.5 MB │ -3.15 MB  │ -1.35 MB │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +52.5 MB │ -3.15 MB  │ -962 kB  │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +52.5 MB │ -3.15 MB  │ -632 kB  │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +51.7 MB │ -3.15 MB  │ -1.29 MB │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss    │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟────────┼───────────┼──────────┼──────────┼──────────────╢
║ +52 MB │ -3.15 MB  │ -958 kB  │ -500 kB  │ -497 kB      ║
╚════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +51.5 MB │ -3.15 MB  │ -1.46 MB │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +51.9 MB │ -3.15 MB  │ -1.13 MB │ -500 kB  │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

@bingtsingw
Copy link

@Brooooooklyn Thank you for you test script, I'll use this method to test my image.

@DailyIsDead
Copy link

I have the same issue, noticed it while periodically checking process.memoryUsage().rss. I have tried the test script provided by @Brooooooklyn and can reproduce the issue. I used a different image, a webp image which is 640.016 bytes and has a resolution of 3840 x 2160px. I used const imgBuf = await readFile("test.webp"); to load the image buffer.

@rambo-panda
Copy link
Contributor Author

when I run it in a container on Ubuntu 22 LTS, there is a severe memory leak.

-import { clearAllCache, createCanvas, loadImage } from "./index.js";
+import { clearAllCache, createCanvas, loadImage } from '@napi-rs/canvas';
+

 const imgBuf = await fetch(
   "https://cdn-ms.17zuoye.cn/zx-ptqlm/testing_2023_10_27/pdf_6d1e8d355613ef824d_0.webp",
-).then((a) => a.arrayBuffer());
+).then((a) => a.arrayBuffer()).then(Buffer.from);

 const initialMemoryUsage = process.memoryUsage()

@@ -45,6 +46,7 @@ function displayMemoryUsageFromNode(initialMemoryUsage) {
   }
   tableData.push(diffColumn)
   console.info(table(tableData))
+	console.info("current RSS value", process.memoryUsage.rss());
 }

image

after 1min

current RSS value 4704.67578125Mb
╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +4.87 GB │ -3.15 MB  │ -1.69 MB │ +2.57 MB │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

current RSS value 4704.93359375Mb
╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +4.87 GB │ -3.15 MB  │ -1.35 MB │ +2.57 MB │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

current RSS value 4705.19140625Mb
╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +4.87 GB │ -3.15 MB  │ -1.02 MB │ +2.57 MB │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

current RSS value 4704.70703125Mb
╔══════════╤═══════════╤══════════╤══════════╤══════════════╗
║ rss      │ heapTotal │ heapUsed │ external │ arrayBuffers ║
╟──────────┼───────────┼──────────┼──────────┼──────────────╢
║ +4.87 GB │ -3.15 MB  │ -1.7 MB  │ +2.57 MB │ -497 kB      ║
╚══════════╧═══════════╧══════════╧══════════╧══════════════╝

current RSS value 4704.96484375Mb

@Brooooooklyn
Copy link
Owner

@rambo-panda your memory leak was caused by then((a) => a.arrayBuffer()).then(Buffer.from), not from @napi-rs/canvas

@rambo-panda
Copy link
Contributor Author

@rambo-panda your memory leak was caused by then((a) => a.arrayBuffer()).then(Buffer.from), not from @napi-rs/canvas

@Brooooooklyn sorry, I didn't understand. Even if I try to download the image locally and then use fs.readFileSync to read it before performing the loadImage operation, the RSS still remains high for a long time.

@DailyIsDead
Copy link

DailyIsDead commented Oct 28, 2024

@rambo-panda your memory leak was caused by then((a) => a.arrayBuffer()).then(Buffer.from), not from @napi-rs/canvas

@Brooooooklyn This script is what I used to test it, I still get a severe memory leak with this, in my comment earlier I loaded a different image via fs.readFile(), now I ran this code and the issue is the same.

import { whiteBright, red, green, gray } from 'colorette';
import prettyBytes from 'pretty-bytes';
import { table } from 'table';

import { clearAllCache, createCanvas, loadImage } from "@napi-rs/canvas";

const imgBuf = await fetch(
    "https://cdn-ms.17zuoye.cn/zx-ptqlm/testing_2023_10_27/pdf_6d1e8d355613ef824d_0.webp",
).then((a) => a.arrayBuffer());

// Tried this too, same result
// import { readFile } from 'node:fs/promises';
// const imgBuf = await readFile("pdf_6d1e8d355613ef824d_0.webp");

const initialMemoryUsage = process.memoryUsage();

const len = ~~(process.argv[2] ?? 50);

console.time("drawImage");
const res = await Promise.all(
    Array(len)
        .fill(0)
        .map(async () => {
            // console.time(i);
            const img = await loadImage(imgBuf);
            const canvas = createCanvas(img.width, img.height);
            canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height);
            await canvas.encode("webp", 80);
            // console.timeEnd(i);
        }),
);
console.timeEnd("drawImage");

function displayMemoryUsageFromNode(initialMemoryUsage) {
    const finalMemoryUsage = process.memoryUsage();
    const titles = Object.keys(initialMemoryUsage).map((k) => whiteBright(k));
    const tableData = [titles];
    const diffColumn = [];
    for (const [key, value] of Object.entries(initialMemoryUsage)) {
        const diff = finalMemoryUsage[key] - value;
        const prettyDiff = prettyBytes(diff, { signed: true });
        if (diff > 0) {
            diffColumn.push(red(prettyDiff));
        } else if (diff < 0) {
            diffColumn.push(green(prettyDiff));
        } else {
            diffColumn.push(gray(prettyDiff));
        }
    }
    tableData.push(diffColumn);
    console.info(table(tableData));
}

setInterval(() => {
    global.gc?.();
    clearAllCache();
    displayMemoryUsageFromNode(initialMemoryUsage);
    // decreasing array length
    res.splice(0, 10);
}, 1000);

memory_leak

npm_list

I let the code run for a while, nothing really changed.

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