bytedeco / javacv

Java interface to OpenCV, FFmpeg, and more
Other
7.53k stars 1.58k forks source link

Mat, Imgproc.resize Possible memory leak #2283

Open Racv opened 2 weeks ago

Racv commented 2 weeks ago

Hi Folks,

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

Environment Details:

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

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

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

Racv commented 2 weeks ago

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

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

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"]

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

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