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

Mat, Imgproc.resize Possible memory leak #2283

Open
Racv opened this issue Sep 15, 2024 · 11 comments
Open

Mat, Imgproc.resize Possible memory leak #2283

Racv opened this issue Sep 15, 2024 · 11 comments

Comments

@Racv
Copy link

Racv commented Sep 15, 2024

Hi Folks,

I’m encountering a possible memory leak while using JavaCV for image resizing in a Spring-WebFlux application.

Environment Details:

  • Kubernetes Pod: 8GB memory
  • JVM Settings: -Xms=1024M, -Xmx=2048M
  • Traffic: ~30 TPS on one pod

Issue: Memory utilization climbs to ~92% over a span of ~30 hours and then stabilises.

  • I’ve ensured that all Mat objects are properly released and also employed PointerScope for memory management.
  • JVM heap memory usage never exceeds 1GB.

Despite these efforts, memory usage continues to rise steadily over time. Below is the snippet of code used for resizing images:

try (PointerScope pointerScope = new PointerScope()) {
    Mat mat = Imgcodecs.imread(inputMediaPath.toString());
    Size size = new Size(width, height);
    Mat resizedMat = new Mat();
    Imgproc.resize(mat, resizedMat, size, 0, 0, Imgproc.INTER_AREA);

    Imgcodecs.imwrite(outputMediaPath.toString(), resizedMat);
    mat.release();
    resizedMat.release();
    // Tried both with and without pointerScope.deallocate(), but memory issues persist.
    pointerScope.deallocate();
}

Any advice on resolving this issue would be greatly appreciated. I’ve tried several approaches but cannot pinpoint the root cause of the rising memory usage.

Thanks in advance for your help!

Best regards,
Ravi

@saudet
Copy link
Member

saudet commented Sep 15, 2024

Please try to set the "org.bytedeco.javacpp.nopointergc" system property to "true".

@Racv
Copy link
Author

Racv commented Sep 15, 2024

Still same behaviour.

I am using 1.5.10 version of javacv. and have also tried setting up these properties.

-Dorg.bytedeco.javacpp.maxPhysicalBytes=1G \
-Dorg.bytedeco.javacpp.maxBytes=512M \
-Dorg.bytedeco.javacpp.maxDeallocatorCache=5M \
-Dorg.bytedeco.javacpp.debug=true \

@saudet
Copy link
Member

saudet commented Sep 16, 2024

Please try to use the C++ API with JavaCPP instead of the Java API of OpenCV because the latter is not very well implemented.

@Racv
Copy link
Author

Racv commented Sep 21, 2024

Thanks for the input @saudet , I tried with simple javacpp API and also for easy debugging, created a small spring boot application with a single controller which only does resize, here also I am seeing similar behaviour. Pointer.physcialBytes() started with 1038M somewhere and now reaching ~1700M after continuous load testing with ~40 TPS for last 13hr.

package com.example.demo;

import jakarta.annotation.PostConstruct;
import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacpp.Pointer;
import org.bytedeco.opencv.global.opencv_imgcodecs;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Size;
import org.bytedeco.opencv.opencv_java;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

@RestController
@RequestMapping("<BasePath>")
public class Controller {

    @PostConstruct
    void init() {
        Loader.load(opencv_java.class);
    }

    @GetMapping(value = "/{mediaTenant}/{mediaId}/{mediaName}")
    public ResponseEntity<StreamingResponseBody> processMedia(@RequestParam Map<String, String> params) throws IOException {
        String inputMediaPath = "media.png";
        Path outputMediaPath = Files.createTempFile("test", ".png");

      
        try (Mat mat = opencv_imgcodecs.imread(String.valueOf(inputMediaPath));
             Mat resizedMat = new Mat();
             BytePointer bytePointer = new BytePointer(String.valueOf(outputMediaPath))) {

            if (mat.empty()) {
                throw new RuntimeException("Could not read the input image.");
            }

            String newWidth = params.get("w");
            String newHeight = params.get("h");
            Size size = new Size(Integer.parseInt(newWidth), Integer.parseInt(newHeight));

            // Resize the image
            opencv_imgproc.resize(mat, resizedMat, size, 0D, 0D, opencv_imgproc.INTER_AREA);

            // Write the resized image to the output path
            opencv_imgcodecs.imwrite(bytePointer, resizedMat);

            // Stream the file as response and clean up after
            StreamingResponseBody responseBody = outputStream -> {
                try (InputStream fileStream = new FileInputStream(String.valueOf(outputMediaPath))) {
                    byte[] buffer = new byte[1024];
                    int bytesRead;
                    while ((bytesRead = fileStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, bytesRead);
                    }
                    outputStream.flush();
                } finally {
                    // Clean up the temporary file
                    Files.deleteIfExists(outputMediaPath);
                }
            };

            mat.release();
            resizedMat.release();
            size.deallocate();
            bytePointer.deallocate();
            HttpHeaders headers = new HttpHeaders();
            headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + outputMediaPath.toFile().getName() + "\"");

            System.out.println(Pointer.physicalBytes()/ (1024*1024));
            return ResponseEntity.ok()
                    .headers(headers)
                    .contentType(MediaType.IMAGE_PNG)
                    .body(responseBody);

        }
    }
}
# Base image with JRE 21 from the private registry
FROM eclipse-temurin:21

RUN mkdir -p /path/to/image/folder

COPY media.png /path/to/image/folder


RUN mkdir -p /opt/dcxp-media-delivery-api /appl/media \
    && apt-get update -y \
        && apt install libjemalloc-dev -y \
         && apt-get install -y libgtk2.0-0 \ # This library is required by opencv.
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

COPY startup.sh /opt/app/startup.sh
RUN chmod +x /opt/app/startup.sh
COPY app.jar /opt/app/app.jar

EXPOSE 8080

# Set the working directory
WORKDIR /opt/app


CMD ["./startup.sh"]
  • startup.sh
#!/bin/bash

export MALLOC_CONF="prof:true,prof_leak:true,lg_prof_interval:30,lg_prof_sample:17,prof_prefix:/opt/app/prof/"

LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so java -Xms1024M -Xmx2048M \
-Dorg.bytedeco.javacpp.maxPhysicalBytes=1G \
-XX:NativeMemoryTracking=detail \
-Dorg.bytedeco.javacpp.nopointergc=true \
-Dorg.bytedeco.javacpp.maxBytes=512M \
-Dorg.bytedeco.javacpp.debug=true \
-jar app.jar

docker run -p 8080:8080 --cpus="4" --memory="3500M" app:2

I have been tracking memory utilisation using docker stats, now it's showing 80% memory consumption.

Also I checked the JVM heap memory, it's not going beyond 1GB

@saudet
Copy link
Member

saudet commented Sep 21, 2024

Please try to use PointerScope: http://bytedeco.org/news/2018/07/17/bytedeco-as-distribution/

@Racv
Copy link
Author

Racv commented Oct 1, 2024

Hi @saudet,

I also tried using PointerScope and observed similar behaviour.

I adjusted the configuration slightly as follows:

java -Xms512M -Xmx1024M \
-Dorg.bytedeco.javacpp.maxBytes=1000M \
-Dorg.bytedeco.javacpp.maxPhysicalBytes=2000M \
-Dorg.bytedeco.javacpp.nopointergc=true \
-jar app.jar

For the Docker container:

  • cpus: 6
  • memory: 6000M

Here's the relevant Java code snippet:

try (PointerScope pointerScope = new PointerScope()) {
    Mat mat = opencv_imgcodecs.imread(String.valueOf(inputMediaPath));
    Mat resizedMat = new Mat();
    BytePointer bytePointer = new BytePointer(String.valueOf(outputMediaPath));
    if (mat.empty()) {
        throw new RuntimeException("Could not read the input image.");
    }

    String newWidth = params.get("w");
    String newHeight = params.get("h");
    Size size = new Size(Integer.parseInt(newWidth), Integer.parseInt(newHeight));

    // Resize the image
    opencv_imgproc.resize(mat, resizedMat, size, 0D, 0D, opencv_imgproc.INTER_AREA);

    // Write the resized image to the output path
    opencv_imgcodecs.imwrite(bytePointer, resizedMat);

    // Stream the file as a response and clean up afterward
    StreamingResponseBody responseBody = outputStream -> {
        try (InputStream fileStream = new FileInputStream(String.valueOf(outputMediaPath))) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fileStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            outputStream.flush();
        } finally {
            // Clean up the temporary file
            Files.deleteIfExists(outputMediaPath);
        }
    };

    mat.deallocate();
    resizedMat.deallocate();
    size.deallocate();
    bytePointer.deallocate();
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + outputMediaPath.toFile().getName() + "\"");

    System.out.println(Pointer.physicalBytes() / (1024 * 1024));
    return ResponseEntity.ok()
            .headers(headers)
            .contentType(MediaType.IMAGE_PNG)
            .body(responseBody);
}

After 40 hours of testing with a load of 50 TPS, I noticed that Pointer.physicalBytes() never exceeded ~1800M, but the container memory usage still reached 96%.

I am not sure what is causing this memory leak. Since javacv provides all the features we need for our application, we are keen to stick with it, but this memory issue is becoming a significant blocker.

Please let me know if there's anything we can do to resolve this issue.

Thanks in advance,
Ravi

@saudet
Copy link
Member

saudet commented Oct 1, 2024

After 40 hours of testing with a load of 50 TPS, I noticed that Pointer.physicalBytes() never exceeded ~1800M, but the container memory usage still reached 96%.

That just sounds like memory fragmentation. How are you sure this is even related to JavaCV?

@Racv
Copy link
Author

Racv commented Oct 1, 2024

Thanks @saudet for immediate reply,

I don't have in-depth knowledge on this topic, but what could be causing this memory fragmentation? The demo application only has one functionality, which is resizing images. Do you have any ideas on what might be causing this issue?

It's just a single spring controller for resizing.

@saudet
Copy link
Member

saudet commented Oct 1, 2024

Reallocating native memory a lot like that can cause memory fragmentation, but there's probably something else going on. If you could reproduce that outside Spring in a standalone application, this is something we could say might be related to JavaCV, but at this point, it could be anything really

@Racv
Copy link
Author

Racv commented Oct 2, 2024

Thanks @saudet , let me try that. Meanwhile I have pushed the demo code here https://github.com/Racv/javacv-demo.

@saudet
Copy link
Member

saudet commented Oct 2, 2024

Also please make sure to set the "org.bytedeco.javacpp.nopointergc" system property to "true".

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

No branches or pull requests

2 participants