From 89ebded63653922402443da468ffcbf6796f2c9c Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Fri, 17 Jul 2026 23:31:48 +0200 Subject: [PATCH] Expand SKMatrix tracking benchmarks for the managed-port surface (#4241) The tracking suite only measured SKMatrix.MapPoints (the batch overload), which is the weakest indicator of the managed SKMatrix port in #4241: the batch does a single native call for the whole array, so the per-call P/Invoke that #4241 removes is amortised to almost nothing. The single-op operations - where the managed<->native transition is paid on every call - are where that win actually shows, and they had no permanent coverage. Split the SKMatrix surface into two tracked groups, mirroring how #4241 changes it: * MatrixMapPointsBenchmark (expanded, geometry mapping) - add MapPoint, MapRect, MapVector and MapRadius, each looped over the existing deterministic point set. The pre-existing MapPoints method and its Points params are left untouched (a shipped history key must never be renamed). All the new methods are allocation-free; MapPoints keeps its allocating batch behaviour as a signal. * MatrixOpsBenchmark (new, matrix algebra) - Concat (pre + post multiply) and Invert, the operations that produce another matrix rather than mapping geometry. Local short-run against the 4.151 nightly shows the single-op methods are dominated by interop overhead (MapPoint 24.6 us vs the MapPoints batch 1.88 us for the same 4096 points; MapRect 55 us, MapRadius 118 us, Concat 125 us, Invert 71 us) - all zero-alloc - so #4241 should visibly drop their time lines while the batch barely moves. The operands are affine (rotation/scale/translate), which is #4241's managed fast path; perspective would fall back to native. Public API only, deterministic (fixed seed), and verified to compile against both the 3.119.4 baseline and 4.151 nightly (no new #if needed). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ca69afac-998b-493d-a03c-d9b25dd3f15b --- .../Benchmarks/MatrixMapPointsBenchmark.cs | 71 +++++++++++++++++-- .../Benchmarks/MatrixOpsBenchmark.cs | 53 ++++++++++++++ 2 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 benchmarks/SkiaSharp.Benchmarks.Tracking/Benchmarks/MatrixOpsBenchmark.cs diff --git a/benchmarks/SkiaSharp.Benchmarks.Tracking/Benchmarks/MatrixMapPointsBenchmark.cs b/benchmarks/SkiaSharp.Benchmarks.Tracking/Benchmarks/MatrixMapPointsBenchmark.cs index f3642f10284..75db722e373 100644 --- a/benchmarks/SkiaSharp.Benchmarks.Tracking/Benchmarks/MatrixMapPointsBenchmark.cs +++ b/benchmarks/SkiaSharp.Benchmarks.Tracking/Benchmarks/MatrixMapPointsBenchmark.cs @@ -2,15 +2,21 @@ namespace SkiaSharp.Benchmarks.Tracking; -// Maps a batch of points through an SKMatrix via the public allocating overload. -// This is a common per-frame transform pattern; the returned array makes the -// allocation a first-class tracked signal alongside the transform time. +// SKMatrix "mapping" operations — applying a matrix to geometry — through the public API. +// MapPoints is the batch (allocating) overload; the single-op MapPoint/MapRect/MapVector/ +// MapRadius each cross managed->native once per call today, so removing that P/Invoke (PR +// #4241 ports the affine path to managed) shows up most starkly on these single-op methods +// — the batch amortises its one native call over all the points and dilutes the win. +// The matrix is affine (rotation), which is #4241's managed fast path (perspective would +// fall back to native). Deterministic (fixed seed). Every single-op method is allocation- +// free; MapPoints keeps its allocating batch behaviour so the returned array stays a signal. public class MatrixMapPointsBenchmark { [Params(256, 4096)] public int Points { get; set; } private SKPoint[] _source = null!; + private SKRect[] _rects = null!; private SKMatrix _matrix; [GlobalSetup] @@ -18,16 +24,73 @@ public void Setup() { var rnd = new Random(42); _source = new SKPoint[Points]; + _rects = new SKRect[Points]; for (var i = 0; i < Points; i++) - _source[i] = new SKPoint((float)rnd.NextDouble() * 1024f, (float)rnd.NextDouble() * 1024f); + { + var x = (float)rnd.NextDouble() * 1024f; + var y = (float)rnd.NextDouble() * 1024f; + _source[i] = new SKPoint(x, y); + _rects[i] = new SKRect(x, y, x + 40f, y + 25f); + } _matrix = SKMatrix.CreateRotationDegrees(30f, 512f, 512f); } + // Batch overload — one native call for the whole array; allocates the result array + // (kept as a tracked allocation signal). Never rename: this is a shipped history key. [Benchmark] public int MapPoints() { var mapped = _matrix.MapPoints(_source); return mapped.Length; } + + // #4241: single-point map, once per source point (one P/Invoke per call today). + [Benchmark] + public float MapPoint() + { + var sink = 0f; + foreach (var p in _source) + { + var m = _matrix.MapPoint(p.X, p.Y); + sink += m.X - m.Y; + } + return sink; + } + + // #4241: map a rectangle (bounds / clip transform) — a very common per-frame op. + [Benchmark] + public float MapRect() + { + var sink = 0f; + foreach (var r in _rects) + { + var m = _matrix.MapRect(r); + sink += m.Width - m.Height; + } + return sink; + } + + // #4241: map a vector (direction / offset — ignores translation). + [Benchmark] + public float MapVector() + { + var sink = 0f; + foreach (var p in _source) + { + var m = _matrix.MapVector(p.X, p.Y); + sink += m.X - m.Y; + } + return sink; + } + + // #4241: map a radius (e.g. a stroke width / blur radius under the transform). + [Benchmark] + public float MapRadius() + { + var sink = 0f; + foreach (var p in _source) + sink += _matrix.MapRadius(p.X); + return sink; + } } diff --git a/benchmarks/SkiaSharp.Benchmarks.Tracking/Benchmarks/MatrixOpsBenchmark.cs b/benchmarks/SkiaSharp.Benchmarks.Tracking/Benchmarks/MatrixOpsBenchmark.cs new file mode 100644 index 00000000000..c9a55cd27df --- /dev/null +++ b/benchmarks/SkiaSharp.Benchmarks.Tracking/Benchmarks/MatrixOpsBenchmark.cs @@ -0,0 +1,53 @@ +using BenchmarkDotNet.Attributes; + +namespace SkiaSharp.Benchmarks.Tracking; + +// SKMatrix "algebra" operations that produce another matrix (as opposed to the geometry +// mapping ops in MatrixMapPointsBenchmark): Concat (pre/post multiply) and Invert. Each +// crosses into native per call today; PR #4241 ports the affine path to managed, so the +// removed P/Invoke shows here. The operands are affine (rotation+scale, translate), which +// is #4241's managed fast path. Deterministic and allocation-free (SKMatrix is a struct). +public class MatrixOpsBenchmark +{ + [Params(4096)] + public int Count { get; set; } + + private SKMatrix _a; + private SKMatrix _b; + + [GlobalSetup] + public void Setup() + { + // A non-trivial affine matrix (rotation composed with a non-uniform scale) and a + // translation — both stay on #4241's managed fast path. + _a = SKMatrix.CreateRotationDegrees(30f, 100f, 100f).PreConcat(SKMatrix.CreateScale(1.5f, 0.75f)); + _b = SKMatrix.CreateTranslation(37f, -19f); + } + + // #4241: matrix multiply, both directions (pre- and post-concat) once per iteration. + [Benchmark] + public float Concat() + { + var sink = 0f; + for (var i = 0; i < Count; i++) + { + var pre = _a.PreConcat(_b); + var post = _a.PostConcat(_b); + sink += pre.TransX - post.TransY; + } + return sink; + } + + // #4241: matrix inverse (hit-testing / mapping back to local coordinates). + [Benchmark] + public float Invert() + { + var sink = 0f; + for (var i = 0; i < Count; i++) + { + if (_a.TryInvert(out var inv)) + sink += inv.ScaleX + inv.TransX; + } + return sink; + } +}