Skip to content

Lossy WebP encoder: Vp8BitWriter.maxPos is never updated, buffer is reallocated every 1 KiB (quadratic allocations) #3206

Closed

Description

@gianlucaratta

Prerequisites

  • I have written a descriptive issue title
  • I have verified that I am running the latest version of ImageSharp
  • I have verified if the problem exist in both DEBUG and RELEASE mode
  • I have searched open and closed issues to ensure it has not already been reported

ImageSharp version

4.1.2 (also present on main)

Other ImageSharp packages and versions

None

Environment (Operating system, version and so on)

Windows 11 Pro

.NET Framework version

.NET 10

Description

Measurements with the repro below (lossy, Quality = 90, default method, .NET 10, Release;
Debug gives the same allocations):

  • 1000x750: output 283 KiB, allocated 76 MiB, 10 gen2 GCs, 0.60 s
  • 2000x1500: output 1132 KiB, allocated 717 MiB, 131 gen2 GCs, 0.84 s
  • 4000x3000: output 4768 KiB, allocated 10,527 MiB, 660 gen2 GCs, 5.62 s
    Output grows ~4x per step, allocations grow ~10-15x: quadratic in the output size.

Steps to Reproduce

using System.Diagnostics;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.PixelFormats;

foreach (var (w, h) in new[] { (1000, 750), (2000, 1500), (4000, 3000) })
{
    using var image = new Image<Rgb24>(w, h);
    var rnd = new Random(42);
    image.ProcessPixelRows(rows =>
    {
        for (int y = 0; y < rows.Height; y++)
        {
            var row = rows.GetRowSpan(y);
            for (int x = 0; x < row.Length; x++)
            {
                byte n = (byte)rnd.Next(0, 40);
                row[x] = new Rgb24((byte)(x * 255 / w + n / 2), (byte)(y * 255 / h + n / 2), (byte)(128 + n));
            }
        }
    });

    var encoder = new WebpEncoder { FileFormat = WebpFileFormatType.Lossy, Quality = 90 };
    using var output = new MemoryStream();
    GC.Collect();
    long before = GC.GetTotalAllocatedBytes(precise: true);
    int gen2Before = GC.CollectionCount(2);
    var sw = Stopwatch.StartNew();
    image.SaveAsWebp(output, encoder);
    sw.Stop();
    long allocated = GC.GetTotalAllocatedBytes(precise: true) - before;
    Console.WriteLine($"{w}x{h}: output {output.Length / 1024} KiB, allocated {allocated / (1024 * 1024)} MiB, " +
                      $"gen2 GCs {GC.CollectionCount(2) - gen2Before}, {sw.Elapsed.TotalSeconds:F2} s");
}

Allocated bytes grow ~quadratically with the output size instead of linearly.

Images

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions