Hi @Syntec,
I’ve confirmed and fixed a memory leak in Picture creation. Your observation that memory kept increasing, and that the occasional slow frame became worse over time, was the key clue.
Each newly created Picture was ending up with one ownership reference too many. When your temporary Picture had been passed to DrawPicture and was no longer needed by your code, that extra reference prevented Objo from reclaiming it. Repeating this line therefore kept accumulating pictures and their associated SVG data:
g.DrawPicture(Picture.FromSVG(SVG), region.X, region.Y)
That is a valid way to draw a temporary picture. Objo should manage its lifetime automatically, so this was a runtime bug.
I reproduced the leak using your static 64 × 64 SVG, creating and discarding 10,000 pictures:
- Before the fix: all 10,000 pictures remained tracked, and managed memory was approximately 329 MiB. Forcing memory collection did not reclaim them.
- After the fix: zero pictures remained tracked, and managed memory was approximately 10 MiB.
Those are managed-memory measurements from a small local test application; the exact totals will vary with the application and machine.
My working explanation for the periodic long frames is that the accumulating pictures made memory-management work progressively more expensive. Objo periodically checks for reference cycles in response to allocation pressure, and repeatedly parsing SVGs also creates allocations that the underlying managed runtime must eventually collect. In a tight benchmark loop, allocation thresholds can be reached at fairly regular intervals, which could explain the roughly one- or two-second pattern you observed.
That fits the increasing memory usage and worsening pauses, but I haven’t reproduced and profiled your precise 112–442 ms spikes. The leak is confirmed; the explanation for the exact timing remains an inference.
I also audited the other Picture creation and storage paths, because correcting the extra reference needed to preserve pictures that were still legitimately in use. The changes cover:
- The shared Picture creation path used by SVGs, image bytes, QR codes, chart exports, clipboard results and plugin exports.
Canvas.ToPicture, which had a separate creation path with the same extra reference.
- Pictures assigned to
Canvas.Backdrop, Chart.Backdrop and ImageViewer.Image, including replacement, clearing and control destruction.
- The cached
Picture.Graphics object, including clipboard pictures that share underlying image state.
There is an important drawing detail here: Canvas drawing commands are replayed later. The temporary Objo Picture can therefore become unused before the UI actually renders its image. The pending drawing command keeps the underlying image available for that later rendering. I added a regression test that releases the temporary pictures, forces a managed collection, and then renders the queued commands to check the resulting pixels.
For performance, Picture.FromSVG still parses the markup and creates a new picture on every call, even when the markup is identical. For static artwork, I recommend creating it once, storing it in a Picture property such as CachedIcon, and reusing it:
g.DrawPicture(CachedIcon, region.X, region.Y)
For changing artwork, create a replacement when the geometry, colours or other SVG content changes. That avoids repeating the parsing work during every repaint. Your existing inline call will also have the correct lifetime after the fix.
One other point about the benchmark: the elapsed time around your current code measures SVG construction, Picture creation and recording the drawing command. The actual Canvas rendering happens afterwards, so converting that elapsed time to FPS gives a rate for that work rather than the number of frames being presented on screen. It is still useful for detecting these pauses, but timing Picture creation separately would help isolate any remaining parser cost.
The fix is committed and scheduled for the next release, tracked as Temporary Pictures no longer leak memory during repeated drawing. Once that build is available, please rerun your original benchmark - the memory trend and whether those periodic long frames remain will be particularly useful.