Brooooooklyn / canvas

High performance skia binding to Node.js. Zero system dependencies and pure npm packages without any postinstall scripts nor node-gyp.
https://vercel.skia.rs
MIT License
1.78k stars 76 forks source link

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

Closed rambo-panda closed 1 week ago

rambo-panda commented 2 months ago
// 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 commented 2 months ago

https://github.com/Brooooooklyn/canvas/issues/867

Horziox commented 2 months ago

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.

bingtsingw commented 2 months ago

I have the same issue

rambo-panda commented 2 months ago

https://github.com/Brooooooklyn/canvas/blob/8ca3a9e55d583aa437996a18e3432ceabd477967/src/image.rs#L417-L424

// 'static pointer
let self_mut = unsafe { Box::leak(Box::from_raw(image_ptr.cast::<Image>())) }; 
rambo-panda commented 2 months ago

https://github.com/Brooooooklyn/canvas/blob/8ca3a9e55d583aa437996a18e3432ceabd477967/src/image.rs#L417-L424

// '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. https://github.com/Brooooooklyn/canvas/issues/868

rambo-panda commented 2 months ago

@Brooooooklyn Hello, excuse me for asking, but what version of rustc are you using for the compilation? I've been using a Docker container created on my home computer, and I find it unable to compile properly.

image

npm run build error log

warning: use of deprecated struct `napi::JsBuffer`: Please use Buffer or &[u8] instead
  --> src/svg.rs:11:13
   |
11 | ) -> Result<JsBuffer> {
   |             ^^^^^^^^

error[E0308]: `?` operator has incompatible types
   --> src/image.rs:242:17
    |
242 |       this_ref: env.create_reference(&this)?,
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Ref<()>`, found `Ref<JsObject>`
    |
    = note: `?` operator cannot convert from `napi::Ref<JsObject>` to `napi::Ref<()>`
    = note: expected struct `napi::Ref<()>`
               found struct `napi::Ref<JsObject>`

error[E0277]: the trait bound `(): napi::NapiValue` is not satisfied
   --> src/image.rs:426:46
    |
426 |     let this: This = env.get_reference_value(&self.this_ref)?;
    |                          ------------------- ^^^^^^^^^^^^^^ the trait `napi::NapiValue` is not implemented for `()`
    |                          |
    |                          required by a bound introduced by this call
    |
    = help: the following other types implement trait `napi::NapiValue`:
              JsArrayBuffer
              JsBoolean
              JsDataView
              JsExternal
              JsFunction
              JsGlobal
              JsNull
              JsNumber
            and 9 others
note: required by a bound in `napi::Env::get_reference_value`
   --> /root/.cargo/registry/src/index.crates.io-6f17d22bba15001f/napi-3.0.0-alpha.9/src/env.rs:884:8
    |
882 |   pub fn get_reference_value<T>(&self, reference: &Ref<T>) -> Result<T>
    |          ------------------- required by a bound in this associated function
883 |   where
884 |     T: NapiValue,
    |        ^^^^^^^^^ required by this bound in `Env::get_reference_value`

error[E0308]: `?` operator has incompatible types
   --> src/image.rs:426:22
    |
426 |     let this: This = env.get_reference_value(&self.this_ref)?;
    |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `JsObject`, found `()`
    |
    = note: `?` operator cannot convert from `()` to `JsObject`

warning: unused import: `NapiRaw`
 --> src/image.rs:5:46
  |
5 | use napi::{bindgen_prelude::*, check_status, NapiRaw, NapiValue, Ref};
  |                                              ^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

Some errors have detailed explanations: E0277, E0308.
For more information about an error, try `rustc --explain E0277`.
warning: `canvas` (lib) generated 9 warnings
The following warnings were emitted during compilation:

warning: canvas@0.1.0: skia-c/skia_c.cpp:455:19: warning: variable length arrays in C++ are a Clang extension [-Wvla-cxx-extension]
warning: canvas@0.1.0:   455 |     SkRect bounds[text_len];
warning: canvas@0.1.0:       |                   ^~~~~~~~
warning: canvas@0.1.0: skia-c/skia_c.cpp:455:19: note: function parameter 'text_len' with unknown value cannot be used in a constant expression
warning: canvas@0.1.0: skia-c/skia_c.cpp:398:14: note: declared here
warning: canvas@0.1.0:   398 |       size_t text_len,
warning: canvas@0.1.0:       |              ^
warning: canvas@0.1.0: 1 warning generated.

error: could not compile `canvas` (lib) due to 3 previous errors; 9 warnings emitted
rambo-panda commented 2 months ago

@Brooooooklyn Hello, excuse me for asking, but what version of rustc are you using for the compilation? I've been using a Docker container created on my home computer, and I find it unable to compile properly.

image

npm run build error log ...

https://github.com/Brooooooklyn/canvas/pull/898/commits/e3ce67e6be5f29b6e0da3d85af9cdeed7a15dc40

bingtsingw commented 1 month ago

@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 commented 1 month ago

@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 commented 1 month ago

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

Brooooooklyn commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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

Brooooooklyn commented 3 weeks ago

@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 commented 3 weeks ago

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

DailyIsDead commented 3 weeks ago

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 commented 3 weeks ago

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 commented 3 weeks ago

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

rambo-panda commented 3 weeks ago

@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 commented 3 weeks ago

@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.

KathyFlores commented 2 weeks ago

same here, memory leaks still exist on 0.1.59

Brooooooklyn commented 1 week ago

Can you test again with 0.1.60? https://github.com/Brooooooklyn/canvas/releases/tag/v0.1.60

DailyIsDead commented 1 week ago

@Brooooooklyn Looks good. My initial tests seem to confirm that the leak is fixed in 0.1.60. I've used the same script to test and I can't find any memory at the moment.

Screenshot 2024-11-10 022211

rambo-panda commented 1 week ago

@Brooooooklyn amazing!! the 0.1.60 significantly alleviated memory issues.

image

However, when I tested with a sample size of 50, I found that although the memory usage decreased significantly compared to version 0.1.59, it still exhibits noticeable slowdowns and some memory is not released.

image

From the above image, it can be seen that 2GB of memory has been reclaimed, but there is still 2GB that has not been reclaimed for a long time.


However, when I start expose-gc, the memory recovery is particularly noticeable.

image

there is my envinfo

  System:
    OS: Linux 3.10 Ubuntu 22.04.1 LTS 22.04.1 LTS (Jammy Jellyfish)
    CPU: (16) x64 Intel Xeon Processor (Cascadelake)
    Memory: 22.42 GB / 31.42 GB
    Container: Yes
    Shell: 5.1.16 - /bin/bash
  Binaries:
    Node: 20.14.0 - /usr/bin/node
    npm: 10.7.0 - /usr/bin/npm
rambo-panda commented 1 week ago

It seems that the memory leak issue has been resolved. Regarding the issue https://github.com/Brooooooklyn/canvas/issues/890#issuecomment-2466625653 mentioned in the comment, I noticed @Brooooooklyn used the napi_adjust_external_memory API.

Here is my understanding of why the remaining 2GB has not been released.(I have not studied NAPI in depth, so if there are any misunderstandings, I hope everyone will enlighten me.)

Using the example of var img = Image(src);, when V8 recovers the JavaScript variable img, it will trigger its destructor function and pass a series of callbacks to the raw_finalize_unchecked method of napi-rs. Here, Rust will recover the Rust object from the raw pointer through data.cast. Then, by following the lifecycle rules, it will recover its memory. Subsequently, napi_adjust_external_memory is used to notify V8 that the amount of external memory has changed. It should be noted that this adjustment only informs V8 of the change in memory usage, but does not necessarily trigger an immediate garbage collection (reduction of this memory usage) by V8.