Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@Ben-D-Anderson
Copy link

@Ben-D-Anderson Ben-D-Anderson commented Oct 25, 2025

The Plan

I've been using the scatter graph and noticed that there were quite a few memory allocations (arrays/lists of size N - where N is the number of points) in both the IScatterSource and the rendering of the IPlottable.

This pull request makes use of the decorator design pattern to create list views that drastically improve the memory performance of scatter graphs, effectively removing any need for garbage collection in the rendering code. Theoretically, the changes should improve the speed at which rendering can take place (given the fact there are less iterations over the points), however, my benchmarks have been all over the place on my laptop so I'm unable to provide evidence of that.

Examples of Changes

How IScatterSource#GetScatterPoints used to be implemented:

public IReadOnlyList<Coordinates> GetScatterPoints()
{
    //this method is called on every render call.
    return Coordinates
        .Skip(MinRenderIndex)
        .Take(this.GetRenderIndexCount())
        .ToList();
    //but this `ToList()` call iterates over every single coordinate in the list, and allocates a new list.
}

How I've changed IScatterSource#GetScatterPoints:

public IReadOnlyList<Coordinates> GetScatterPoints()
{
    //still called on every render call but no longer expensive.
    return Coordinates
        .SkipView(MinRenderIndex)
        .TakeView(this.GetRenderIndexCount());
    //no allocations, no iterations, just a pure view over the original data.
}

All the list view extension methods can be found in ScottPlot.DataSources.ReadOnlyListExtensions.

Iterations over the data were also removed from ScottPlot.Plottables.Scatter.
Here's an extract from the old implementation:

public virtual void Render(RenderPack rp)
{
    var coordinates = Data.GetScatterPoints();
    Pixel[] markerPixels = new Pixel[coordinates.Count];
    for (int i = 0; i < coordinates.Count; i++)
    {
        double x = coordinates[i].X * ScaleX + OffsetX;
        double y = coordinates[i].Y * ScaleY + OffsetY;
        markerPixels[i] = Axes.GetPixel(new(x, y));
    }
    //remainder omitted...
}

And here's how I changed it:

public virtual void Render(RenderPack rp)
{
    var coordinates = Data.GetScatterPoints();
    //now creating pixels from coordinates using a view instead of iterating over them
    IReadOnlyList<Pixel> markerPixels = coordinates.SelectView(coordinate =>
        Axes.GetPixel(new(coordinate.X * ScaleX + OffsetX, coordinate.Y * ScaleY + OffsetY)));
    //remainder omitted...
}

Benchmarks

I haven't been able to see improvements from a time perspective despite the notable reduction in iterations over the coordinates - this is either due to the lack of repeatability on my laptop, or the code isn't as fast as I think it is.
However, one thing that is consistent, is the significant reduction in memory footprint for a larger number of points. This approach ends up yielding an O(1) space complexity instead of the previous O(N).

Benchmarks Before Change:

| Method       | Points | Mean       | Error     | StdDev    | Gen0    | Gen1   | Allocated  |
|------------- |------- |-----------:|----------:|----------:|--------:|-------:|-----------:|
| ScatterLines | 100    |   1.454 ms | 0.0363 ms | 0.0989 ms | 35.1563 | 1.9531 |  219.28 KB |
| ScatterLines | 1000   |   6.432 ms | 0.0482 ms | 0.0451 ms | 39.0625 |      - |  241.12 KB |
| ScatterLines | 10000  |  56.646 ms | 0.2530 ms | 0.2367 ms |       - |      - |  452.56 KB |
| ScatterLines | 100000 | 525.849 ms | 9.0735 ms | 8.0434 ms |       - |      - | 2568.58 KB |

Benchmarks After Change:

| Method       | Points | Mean       | Error     | StdDev    | Gen0    | Gen1   | Allocated |
|------------- |------- |-----------:|----------:|----------:|--------:|-------:|----------:|
| ScatterLines | 100    |   1.467 ms | 0.0114 ms | 0.0096 ms | 35.1563 | 1.9531 | 216.91 KB |
| ScatterLines | 1000   |   6.224 ms | 0.0190 ms | 0.0148 ms | 31.2500 |      - | 217.62 KB |
| ScatterLines | 10000  |  53.935 ms | 1.0543 ms | 1.5781 ms |       - |      - | 218.17 KB |
| ScatterLines | 100000 | 528.675 ms | 9.2476 ms | 8.6502 ms |       - |      - |  224.8 KB |

Garbage Collection During Rendering (at 10,000 points) Before Change:
image

Garbage Collection During Rendering (at 10,000 points) After Change:
image

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant