.agents/skills/performance-fixer/SKILL.md
Proactively find and fix performance problems in SkiaSharp — a thin managed wrapper over native Skia, so its recurring, high-impact family is the managed layer's own overhead between the caller and Skia: a P/Invoke transition paid for math that is a few float ops, an allocation on a hot parse/convert path, a native lookup redone on every getter, per-element marshalling in a loop. This is not about making Skia's C++ rasterizer faster (that is upstream); it is about removing the tax the C# layer imposes. Every fix is measured (a benchmark) and behaviour-preserving (an equivalence test).
Scope: managed C# only — binding/** and source/**. Everything under externals/skia/**
(including our C shim) is upstream Skia: out of scope to edit or build, though you may read the
pinned source to verify an invariant. Every candidate must be provable and fixable from C#.
Read references/decision-framework.md (is it worth it? the
impact×complexity rubric + the two-proof gate) and references/measuring.md
(how to prove faster and identical) first — they are the model this skill runs on. Background on
the interop boundary is in documentation/dev/memory-management.md
and documentation/dev/architecture.md.
New vs Old shows a meaningful, repeatable speedup with no allocation
regression; an equivalence test proves the result is identical to the original/native path
(bit-exact for numeric ports) across normal and edge inputs. No speedup ⇒ nothing to fix. Any
behaviour change ⇒ reject — a faster answer that differs from Skia is a rendering regression.[Obsolete]-hide, or delete a test. If a correctness test goes red,
fix the change, not the test.*.generated.cs and externals/skia/** are
off-limits to edit/build. You may READ the pinned Skia C++ (fetch at the submodule's pinned
commit and cite it) to verify an algorithm or pointer-stability invariant.ReadOnlySpan<char> overloads.)RuntimeInformation-gated static readonly bool, as #4241 did). Never ship a
float port without it.noop.Utils.RentArray, RentHandlesArray, SKString) or the native oracle may already fit.Low complexity, high impact. Prefer them whenever you write or touch hot-path code.
Try* overload over the allocating one; add a ReadOnlySpan<char> overload where
only the string/T[] one exists (additive, ABI-safe).capacity, rent from Utils.RentArray/ArrayPool.stackalloc a small, bounded buffer instead of allocating (cap the size; never in a loop).SearchValues<T> for repeated set search, FrozenDictionary for
build-once maps.sealed internal types, [MethodImpl(AggressiveInlining)] on trivial wrappers,
in/ref readonly on large structs (internal / new overloads only), avoid LINQ/boxing in loops.High complexity — apply only on a proven hot path, behind a clean API, with the two proofs. Even when you recommend the simpler option, report the faster high-complexity one and its tradeoff.
Vector128/Vector256 (ARM64 NEON Vector256 was 5.7–6.5× slower in #4241).unsafe, raw pointers, MemoryMarshal.Cast/Unsafe.As reinterpretation.HandleDictionary locking discipline.Route here from signals.md; each file has the where to look grep, the slow→fast, the watch-out, and the real PR.
| SkiaSharp area | Reference |
|---|---|
| Geometry & math (SKMatrix/SKRect/SKPoint native-math ports) | hot-paths/geometry-math.md |
| Color parse/convert (SKColor/SKColorF, span overloads) | hot-paths/color.md |
| Handles & collections (getter caching, sizing, HandleDictionary) | hot-paths/handles-and-collections.md |
| Text & fonts (glyph loops, HarfBuzz marshalling, shaping memoization) | hot-paths/text-and-fonts.md |
| Pixels & images (bitmap/pixmap bulk copy, blittable reinterpret) | hot-paths/pixels-and-images.md |
The general .NET fast-API guidance behind the patterns above, with TFM guards.
| Area | Reference |
|---|---|
| Strings & spans | bcl-patterns/strings-and-spans.md |
| Numerics, SIMD & codegen | bcl-patterns/numerics-and-simd.md |
| Memory & buffers | bcl-patterns/memory-and-buffers.md |
| Collections & searching | bcl-patterns/collections.md |
| Interop & marshalling | bcl-patterns/interop-and-marshalling.md |
| You were asked to… | Do this |
|---|---|
| Scan and fix (the default; what the agentic workflow runs) | Phases 0 → 5 below: hunt → prove faster → implement + prove identical → file the finding + a linked draft PR (Fixes #…). |
| Find an opportunity (scan only) / file an issue | Phases 0 → 2, then file a [performance] issue with the numbers, framed as an unvalidated hypothesis — a benchmarked proposed fast path is not yet proof of behaviour parity. Don't use "proven/fixable" language without the Phase 3 parity proof. |
| Author or review perf code interactively (a human is driving) | Route via signals.md, apply low-complexity hot-path wins inline, and report medium/high ones with their tradeoff. Still hold the two-proof bar before claiming a win. |
dotnet cake --target=externals-download (pre-built natives; you never build native). The benchmark
harness is benchmarks/SkiaSharp.Benchmarks — read benchmarks/README.md
and copy Benchmarks/TemplateBenchmark.cs to start; the test project is tests/SkiaSharp.Tests.Console.
See measuring.md and repo-helpers.md.
1.1 Pick a focus area (round-robin). If the run supplies an explicit focus area (a bare number 0–4), use it and skip rotation. Otherwise rotate over the 5 hot-path areas so consecutive runs differ:
DOY=$(date -u +%j); HOUR=$(date -u +%H) # zero-padded day-of-year + hour
FOCUS=$(( (10#$DOY * 24 + 10#$HOUR) % 5 )) # 10# forces base-10; 0..4
echo "focus area: $FOCUS" # 0 geometry-math · 1 color · 2 handles-and-collections · 3 text-and-fonts · 4 pixels-and-images
Open that hot-paths/ reference and its Where to look grep. Widen to a neighbour only if it's
exhausted.
1.2 Establish the hot path and cost — with file:line citations: the realistic caller and how
often it runs; the concrete overhead (which the reference names); and the invariant that makes the
fast path still correct. If you can't name that invariant, drop it. Skip anything already optimized
(the references list the hardened spots).
1.3 De-dup against open issues/PRs (search the [performance] prefix and the specific
type/API name — real perf work is often perf(...)/Optimize …):
gh issue list --repo "$GITHUB_REPOSITORY" --search '"[performance]" in:title' --state open --json number,title
gh pr list --repo "$GITHUB_REPOSITORY" --search 'SKMatrix in:title' --state open --json number,title
Respect in-flight work (#4241 SKMatrix, #4276/#3699 bench CI, #3489 CopyTo, #4182 dict sizing,
#3033 DrawShapedText). Pick the ONE strongest candidate; if none convinces, stop (noop).
Follow measuring.md §"Proof 1": a New vs Old benchmark in one process,
[MemoryDiagnoser], realistic workload, statistical rigor (Mean/Error/StdDev, ≥2 runs, no alloc
regression, no regression on any real shape). No measurable/repeatable win ⇒ not a finding.
Write the equivalence test first (measuring.md §"Proof 2") — full
behaviour parity (return value bit-exact for numeric ports; edge inputs; exceptions/validation;
ownership/GC.KeepAlive; rendered pixels), confirmed to catch a deliberately-wrong result. Then
implement the minimal fix using the matching hot-path + bcl-pattern references, honouring that
family's Watch out and all TFMs (guard newer APIs; a float port keeps the x87 fallback).
Confirm: identical (equivalence passes), faster (benchmark holds), no regressions (type's test class
Self-review gate — before the PR (all must tick, else fix or noop):
binding/**/source/** only — no *.generated.cs, no externals/skia/**.Two linked safe outputs so the finding auto-closes on merge:
create_issue, temporary_id like aw_perf1) — the hot path + measured cost
(family, file:line, the realistic caller, the Phase 2 benchmark table, the scope note).create_pull_request, draft, branch dev/perf-<desc>) — the fix (what changed + the
invariant that keeps it correct), proof faster (benchmark table + command), proof identical
(the equivalence test + what edges it covers + that it catches a wrong result), and Fixes #aw_perf1
on its own line.tenet/performance; add the matching perf/*
sub-type chosen by the dominant, measured driver of the win (a removed P/Invoke → perf/interop,
removed managed allocations → perf/allocations, else perf/rendering/perf/throughput/
perf/startup/perf/memory-leak/perf/size). Canonical taxonomy:
.agents/skills/issue-triage/references/labels.md. Usually one sub-type. When run from the agentic
workflow, its guardrail 8 restates this.Short summary: area, candidate (file:line), benchmark result (New vs Old, ratio, allocations),
equivalence coverage, and the issue + PR links — or "no convincing candidate this run". End with the
right safe output: the issue + PR pair, the issue alone (native/upstream), or a single
noop (quiet/dry run). Never finish with no safe output.