mono / SkiaSharp

SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library. It provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.
MIT License
4.45k stars 540 forks source link

[BUG] SKObject does not dispose or release memory on linux #1982

Open PhotonSPK opened 2 years ago

PhotonSPK commented 2 years ago

Description

When I was using SkiaSharp on Linux, I noticed that the memory footprint didn't go down, it just kept going up with each call. When I tried to reproduce the bug on Windows, the memory usage didn't go up.

Code I used following code to test on Windows11 x64, Debian 10 x64, and Debian 11 arm64.

static int Main()
    {
        while (true)
        {
            var key = Console.ReadKey();
            switch (key.Key)
            {
                case ConsoleKey.T:
                    Test();
                    break;

                case ConsoleKey.D:
                    TestWithDispose();
                    break;

                case ConsoleKey.G:
                    GC.Collect();
                    break;

                case ConsoleKey.Q:
                    return 0;

                default:
                    break;
            }
            Console.WriteLine($"{Environment.WorkingSet / 1024 / 1024} MB");
        }
    }

    static void Test()
    {
        SKBitmap bitmap = SKBitmap.Decode("sample.gif");
        SKCanvas sKCanvas = new SKCanvas(bitmap);
        return;
    }

    static void TestWithDispose()
    {
        SKBitmap bitmap = SKBitmap.Decode("sample.gif");
        SKCanvas sKCanvas = new SKCanvas(bitmap);
        bitmap.Dispose();
        sKCanvas.Dispose();
        return;
    }

Expected Behavior

After every Test() and GC.collect(), the memory footprint should be kept low.

Actual Behavior

On Windows it works as it should be. But on Linux, the memory footprint is always kept at a high level, even when GC.collect() is called. Just like those objects are always cached and can't be released.

Basic Information

Screenshots here is my test on Windows 图片

and here is my test on Debian 11 (Raspberry Pi 4B) 图片

PhotonSPK commented 2 years ago

Additionally, I did tests with SkiaSharp.NativeAssets.Linux 2.88.0-preview.179 and SkiaSharp 2.88.0-preview.179, which still resulted in consistently increasing memory usage.

I think the problem is related to libSkiaSharp.so, because the problems that have occurred so far have been on the Linux platform.

I don't have a device to test on macOS , if so, I'll update this issue.

pardont commented 2 years ago

Is there any update? Same problem with SkiaSharp 2.80.2 on Ubuntu 20.04.

mattleibow commented 2 years ago

Are you testing with mono or .net core on linux? I wonder if there is just the fact that mono does not feel the need to clear things or does not reduce the indicated memory? Are you using .net framework or dotnet core on windows?

PhotonSPK commented 2 years ago

Are you testing with mono or .net core on linux? I wonder if there is just the fact that mono does not feel the need to clear things or does not reduce the indicated memory? Are you using .net framework or dotnet core on windows?

I'm using dotnet 6.0 both on Windows and Linux.

PhotonSPK commented 2 years ago

This bug confused me for weeks, until I found this, which mentioned that this is a problem with memory allocator. https://github.com/dotnet/runtime/issues/13301#issuecomment-535641506

After setting MALLOC_TRIM_THRESHOLD_ , I could control my memory usage in a acceptable range. But unlike on Windows.and MacOS(i tested it on macos, same as windows) , the memory usage doesn't go down to a low level after calling ,GC.collect(), it seemed being limited at a certain level.In my case it goes up to as test going, then stuck at 300MB.

For somehow it did fixed my problem, but i wonder if there's a better solution.

sharpSteff commented 1 year ago

I have the same issue with a quite similar setup => .NET 6 and Ubuntu 20.04.

I just load a file from disk, copy it and dispose both instances. DotMemory shows following:

memory

It seems SKShader hangs arround and will be finalized by the GC. Is this the intended behavior? I expect SkiaSharp to free any resources immediatelly when calling Dispose(). image

using SkiaSharp; 
for (int i = 0; i < 10000; i++)
{
    Console.WriteLine($"Run: {i}");
    Console.WriteLine("Load skiasharp from file!");
    using var filestream = File.Open("navi.PNG", FileMode.Open);
    var bitmap = SKBitmap.Decode(filestream);
    var copy = bitmap.Copy();
    bitmap.Dispose();
    copy.Dispose();
}

Edit: if I use my own copy implementation, which disposes the SKShader, the memory spikes are gone and everything is freed up:

SKBitmap CopyFixed(SKBitmap bitmap, SKColorType colorType)
{
    using var srcPixmap = bitmap.PeekPixels ();

    var temp = new SKBitmap ();

    var dstInfo = srcPixmap.Info.WithColorType (colorType);
    if (!temp.TryAllocPixels (dstInfo))
        return null;

    using var canvas = new SKCanvas (temp);

    using var shader =  bitmap.ToShader();
    using var paint = new SKPaint {
        Shader = shader,
        BlendMode = SKBlendMode.Src
    };

    canvas.DrawPaint (paint);
    return temp;
}

I don't know if there are any edge-cases, in which this change might break something, but for me this is good enough.

gktval commented 1 year ago

This seems related: I kept having problems with the bitmap being in use when calling bitmap.Copy(). This occurred in both Windows and Android. Calling GC.Collect() fixed the 'in use exception'. It would only happened randomly though. about 1/4 of the time. Here is my relevant code:

var info = new SKImageInfo(width + 1, height + 1, SKImageInfo.PlatformColorType, SKAlphaType.Unpremul);
var outbmp = new SKBitmap(info);
var BMArray = new byte[(info.RowBytes * info.Height)];
...load data into the array...
// pin the managed array so that the GC doesn't move it
var handle = GCHandle.Alloc(BMArray, GCHandleType.Pinned);

// install the pixels with the color type of the pixel data
outbmp.InstallPixels(info, handle.AddrOfPinnedObject(), info.RowBytes);
handle.Free();
... Sometime later call 
var copyBitmap = outbmp.Copy();

Calling GC.Collect(); after freeing the handle has fixed my issue. So, it seems that after freeing the handle, something was still being held onto the bitmap.

ggolda commented 1 year ago

Have the same issue, on Ubuntu 22.04. Looks like calling Dispose is not freeing any resources and memory keeps growing until process is killed in the end.