Does GC.Collect work in Unity6's WebGL? (It didn't seem to work in previous versions.)

Hello,

I have always had issues with WebGL not clearing memory (Version 2021.3.9f).

  • When I download large images and write code to release them, it doesn’t work. After repeating it several times, the release fails and a memory error occurs.
    (Even calling GC.Collect doesn’t resolve the issue.)
  • At first, it seemed to work, but when refreshing + downloading a large image + deleting it + downloading another large image repeatedly, it always leads to an error. (It keeps increasing until it reaches 2GB.)

Does the new Unity 6 WebGL clear memory?

Since mobile support has been added, I wanted to test it, but I’m concerned about the memory issues.

If anyone knows, please reply!

I cant say i noticed the same. Maybe im just not making a ton of garbage.

I want to create something that minimizes downloads as much as possible.
However, I also want to make something where people can upload their own images to view them, similar to a simulation. This inevitably increases the number of downloads.

Has anyone experienced the same issue (where GC.Collect does not work) when using Unity 6?

Hi,
Calling GC.collect() manually will not work on the Web platform. This is a limitation how the garbage collector works on the Web Platform. Refer to this page for more information: Unity - Manual: Memory in Unity Web

On the Web Platform garbage can only be collected when no managed code is executed, i.e., when a frame has ended and a new frame has not started yet. In order to work around this you need to avoid allocating lots of temporary memory within a single frame, e.g., allocating small, short-lifed objects inside a loop.

For the concrete issue you are having: how does your image loading code look like? Do you download multiple images at the same time within a single frame? Would it be possible for you to use coroutines to split the loading of the images across multiple frames? This would give the garbage collector time to free memory in between frames.

1 Like

This is my code.
I also have code that converts rectangles into squares.
I uploaded a 30.8MB (6378x19370, jpeg) image for testing (for an extreme test…).

After loading large images a few times, a memory error eventually occurs.

//Call Function   
    public void ChangeImage(MeshRenderer meshRender , string imgUrl, string propertiesName="_BaseMap") 
    {
        if (imgCo!=null ) 
        {
            StopCoroutine(imgCo);
        }

        if (imgUrl.Length <= 0)
        {
            meshRender.material.SetInt("_useImage", 1);
            meshRender.material.SetTexture(propertiesName, clearTexture);
        }
        else 
        {
            imgCo = StartCoroutine(DownloadImg(imgUrl, (texture) =>
            {
                Texture resizeTexture = RectToSqure(texture);
                meshRender.material.SetInt("_useImage", 1);
                meshRender.material.SetTexture(propertiesName, resizeTexture);
            }));
        }

    }


//Download Image     
public IEnumerator DownloadImg(string url, Action<Texture2D> GetTexture) 
    {
        using (UnityWebRequest www = UnityWebRequestTexture.GetTexture(url))
        {
            yield return www.SendWebRequest();
            if (www.result == UnityWebRequest.Result.Success)
            {
                Texture2D texture = ((DownloadHandlerTexture)www.downloadHandler).texture;
                GetTexture(texture);

                imgCo = null;
            }
            else
            {
                Debug.Log("DownloadError :" + url);
            }


            www.Dispose();

        }

        yield return null;
    }

public Texture2D RectToSqure(Texture2D inputTexture, bool isflip=false)
    {
        if (inputTexture == null)
        {
            Debug.Log("InputTexture null ");
            return null;
        }

        Mat originalMat = new Mat(inputTexture.height, inputTexture.width, CvType.CV_8UC4);
        OpenCVForUnity.UnityUtils.Utils.texture2DToMat(inputTexture, originalMat);

        DestroyImmediate(inputTexture);
        inputTexture = null;

        originalMat = ResizeIfNeeded(originalMat, 2048, 2048);
        int maxLength = Mathf.Max(originalMat.width(), originalMat.height());
        Mat squareMat = new Mat(maxLength, maxLength, originalMat.type(), new Scalar(0, 0, 0));

        //
        int dx = (maxLength - originalMat.width()) / 2;
        int dy = (maxLength - originalMat.height()) / 2;
        //Mat submat = squareMat.submat(dy, dy + originalMat.height(), dx, dx + originalMat.width());
        originalMat.copyTo(squareMat.submat(dy, dy + originalMat.height(), dx, dx + originalMat.width()));

        if (isflip==true) 
        {
            Core.flip(squareMat, squareMat, 0);
        }
        
        Texture2D outputTexture = new Texture2D(squareMat.cols(), squareMat.rows(), TextureFormat.RGBA32, false);
        OpenCVForUnity.UnityUtils.Utils.matToTexture2D(squareMat, outputTexture);

        originalMat.release();
        squareMat.release();
        originalMat = null;
        squareMat = null;

        return outputTexture;
    }

    Mat ResizeIfNeeded(Mat image, int maxWidth, int maxHeight)
    {
        // Current Size
        Size size = image.size();

        //When image size big
        if (size.width > maxWidth || size.height > maxHeight)
        {
            float aspectRatio = (float)size.width / (float)size.height;
            int newWidth, newHeight;

            if (aspectRatio > 1)
            {
                newWidth = maxWidth;
                newHeight = (int)(maxWidth / aspectRatio);
            }
            else
            {
                newHeight = maxHeight;
                newWidth = (int)(maxHeight * aspectRatio);
            }

            Imgproc.resize(image, image, new Size(newWidth, newHeight));
        }
        return image;
    }

Are you making sure to clean up the materials for your mesh renderers?

You’re accessing its .material property, which will create a material instance that you will need to manually Destroy later.

Same with the textures. Are you destroying those later as well?

1 Like

Hello! Thanks for your response!

When I assign a texture to a material, does it create a new material instance?

In the Inspector, it only shows as “myMat(clone)” and doesn’t keep stacking like “myMat(clone)(clone),”
but internally, is it stacking like “myMat(clone) (1)” > (change Material Texture) > “myMat(clone) (1)” “myMat(clone) (2)”?

According to [spiney199], before doing meshRenderer.material.SetTexture(propertiesName, resizeTexture);, it’s recommended to:

Texture resizeTexture = RectToSquare(texture);
Destroy(meshRenderer.material.GetTexture(propertiesName)); // Destroy the existing texture!
meshRenderer.material.SetTexture(propertiesName, resizeTexture);

Should I delete it this way?
Currently, it’s not set up like this.

No I don’t mean that at all.

The moment you touch the .material property of a renderer component, Unity creates an instance of the material. Accessing it further will be accessing the instance, rather than making additional instances.

You need to hold onto a reference to this material, and Destroy the material when its no longer needed. For example, when modifying the material of an instanced prefab. You should destroy the material when the prefab is destroyed as well.

Same with downloaded textures. They are are effectively being new()-ed as well, so you need to Destroy them when no longer needed.

1 Like

If I understand your code right you are using the OpenCV for Unity package to resize the images and make them square?
It looks like you need to create a lot of temporary buffers to do the resizing. An uncompressed 6378x19370 image at 4 byte pixel per pixel would be around 470 MB in size which would explain why you can run out of memory quickly.
I haven’t work with this package before and don’t know how it handles memory internally but I would suspect it could be a culprit for the increasing memory usage.

Can you try to create a simple project that just does something like this and see if this also causes a memory leak?

public IEnumerator TestOpenCVMemoryAllocation()
{
  while (true)
  {
    Mat mat= new Mat(6378, 19730, CvType.CV_8UC4);
    mat.Release();

    // Wait for next frame
    yield return null;
  }
}

Edit:
I looked at the documentation of the package and it looks like the C# wrapper of Mat implements IDisposable. Could you try using mat.Dispose() or using var Mat mat= new Mat(6378, 19730, CvType.CV_8UC4); instead of calling mat.Release() and see if this solves your issue?