performance-fixer
mono/SkiaSharp/.agents/skills/performance-fixer/SKILL.md
Scan SkiaSharp for managed-C# performance opportunities AND fix them, proving each with a BenchmarkDotNet measurement plus a behaviour-parity test. Two modes: (1) SCAN — hunt the SkiaSharp perf signature (pure math round-tripping through native P/Invoke, an allocating parse/convert helper or missing Span overload, a hot getter redoing native lookups every call, per-element interop in a loop, avoidable marshalling/struct copies, or an unsized/ contended collection) and prove the win with a benchmark; (2) FIX — implement the minimal managed optimization, prove it is faster AND behaviour-identical, and open a PR.
What's in it
- Performance Fixer
- Golden rules (non-negotiable)
- How to use this skill
- The cheap wins (apply by default on hot paths)
- Be cautious with (measure first, isolate, keep all TFMs safe)
- Hot-path references — where the wins live (primary)
- BCL pattern references — the techniques (foundation)
- Mode selection
- The autonomous workflow (scan → prove → fix → file)
- Phase 0 — Prepare the scan (no native download)
- Phase 1 — Scan (find ONE candidate)
- Phase 2 — Prove it is faster
- Phase 3 — Fix + prove identical
- Phase 4 — File the finding, then the linked fix PR
- Phase 5 — Report
---
name: performance-fixer
description: >
Scan SkiaSharp for managed-C# performance opportunities AND fix them, proving each with a
BenchmarkDotNet measurement plus a behaviour-parity test. Two modes: (1) SCAN — hunt the
SkiaSharp perf signature (pure math round-tripping through native P/Invoke, an allocating
parse/convert helper or missing Span overload, a hot getter redoing native lookups every
call, per-element interop in a loop, avoidable marshalling/struct copies, or an unsized/
contended collection) and prove the win with a benchmark; (2) FIX — implement the minimal
managed optimization, prove it is faster AND behaviour-identical, and open a PR.
Triggers: "performance", "perf scan", "optimize", "make it faster", "hot path", "reduce
allocations", "P/Invoke overhead", "interop overhead", "speed up", "port to managed", "add
Span overload", "cache the wrapper", "why is this slow", any request to find or fix
SkiaSharp managed performance problems.
For a functional bug use `issue-fix`; for a memory/disposal leak use `memory-leak-fixer`.
---
# Performance Fixer
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`](references/decision-framework.md) (is it worth it? the
impact×complexity rubric + the two-proof gate) and [`references/measuring.md`](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`](../../../documentation/dev/memory-management.md)
and [`documentation/dev/architecture.md`](../../../documentation/dev/architecture.md).
## Golden rules (non-negotiable)
1. **One optimization per run.** Pick the single strongest candidate; a perf PR is only reviewable
as one before/after with one benchmark.
2. **Two proofs, always — speed AND correctness** (details in [measuring.md](references/measuring.md)):
a BenchmarkDotNet `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**.
3. **Never trade correctness for speed.** No "approximation", no dropped edge case
(NaN/±0/Inf/degenerate/overflow), no changed rounding, no skipped validation. If the only way
faster changes what the method returns, **stand down**.
4. **Never weaken, skip, mute, `[Obsolete]`-hide, or delete a test.** If a correctness test goes red,
fix the change, not the test.
5. **Never edit generated files or upstream Skia.** `*.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.
6. **ABI stability.** Change method **bodies** or add **overloads**; never change/remove a public
signature. (#4241 changed only bodies; #4345 added `ReadOnlySpan<char>` overloads.)
7. **Float determinism across runtimes.** A managed port of native float math is bit-exact only on
SSE2/NEON runtimes; **x86 .NET Framework (x87) diverges** — any float port must keep a native
fallback there (a `RuntimeInformation`-gated `static readonly bool`, as #4241 did). Never ship a
float port without it.
8. **Honest, numeric scope note.** Report the **actual measured numbers** (Mean/Error/StdDev,
allocations, ratio) on named hardware/TFM; say what is *empirically measured* vs *statically
reasoned*, plus ABI impact. Never claim a speedup you did not measure.
9. **Finding nothing is the expected outcome.** SkiaSharp is mature; most obvious overhead is already
optimized. Most runs should end with **no candidate**. A 2% win on a synthetic micro-loop no real
caller hits is **not** a finding. A quiet run is a first-class success — emit a `noop`.
## How to use this skill
1. **Decide if it's worth it.** [decision-framework.md](references/decision-framework.md): be
aggressive with low-complexity wins on hot paths; reserve high-complexity (native-math ports,
SIMD, caching) for measured cases. Confirm a **realistic hot caller** first.
2. **Reuse before you build.** [repo-helpers.md](references/repo-helpers.md) — a shared helper
(`Utils.RentArray`, `RentHandlesArray`, `SKString`) or the native oracle may already fit.
3. **Route from the signal.** [signals.md](references/signals.md) maps *what the code does* → the
hot-path / bcl-pattern reference that covers it.
4. **Prove it.** [measuring.md](references/measuring.md) — both proofs, against this repo's harness.
## The cheap wins (apply by default on hot paths)
Low complexity, high impact. Prefer them whenever you write or touch hot-path code.
- Prefer the span/`Try*` overload over the allocating one; add a `ReadOnlySpan<char>` overload where
only the `string`/`T[]` one exists (additive, ABI-safe).
- Pre-size and pool: give collections a `capacity`, rent from `Utils.RentArray`/`ArrayPool`.
- `stackalloc` a small, **bounded** buffer instead of allocating (cap the size; never in a loop).
- Cache a stable native wrapper across calls when the four preconditions hold (pointer identity,
lifetime, disposal invalidation, thread model).
- Size the specialized type: `SearchValues<T>` for repeated set search, `FrozenDictionary` for
build-once maps.
- Let the JIT help: `sealed` internal types, `[MethodImpl(AggressiveInlining)]` on trivial wrappers,
`in`/`ref readonly` on large structs (internal / new overloads only), avoid LINQ/boxing in loops.
## Be cautious with (measure first, isolate, keep all TFMs safe)
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.
- Porting native float math to managed C# (bit-exact + the x87 fallback).
- Manual SIMD / `Vector128`/`Vector256` (ARM64 NEON `Vector256` was **5.7–6.5× slower** in #4241).
- `unsafe`, raw pointers, `MemoryMarshal.Cast`/`Unsafe.As` reinterpretation.
- Any change to the `HandleDictionary` locking discipline.
## Hot-path references — where the wins live (primary)
Route here from [signals.md](references/signals.md). Start with the selected `FOCUS` row, open only
its linked reference, then use that file's full *Where to look* commands. Each reference also has
the slow→fast, watch-out, and real PR.
| `FOCUS` | SkiaSharp area | Where to look | Reference |
|---:|---|---|---|
| 0 | Geometry & math | Pure managed math on blittable value types in `binding/SkiaSharp/`, such as `SKMatrix.cs`, `MathTypes.cs`, `SKColorF.cs`, and `SKPMColor.cs`. | [hot-paths/geometry-math.md](references/hot-paths/geometry-math.md) |
| 1 | Color parse / convert | Parse, format, and conversion helpers in `binding/SkiaSharp/` and `binding/HarfBuzzSharp/`. | [hot-paths/color.md](references/hot-paths/color.md) |
| 2 | Handles & collections | Native-wrapper getters and object tracking in `binding/SkiaSharp/`, including `GetObject`, `OwnedBy`, and `HandleDictionary` paths. | [hot-paths/handles-and-collections.md](references/hot-paths/handles-and-collections.md) |
| 3 | Text & fonts | Per-glyph/per-draw loops, string or array marshalling, and repeated invariant shaping work in `binding/SkiaSharp/` and `binding/HarfBuzzSharp/`. | [hot-paths/text-and-fonts.md](references/hot-paths/text-and-fonts.md) |
| 4 | Pixels & images | Bulk pixel/scanline paths and array materialization in `SKBitmap.cs`, `SKPixmap.cs`, and `SKImage.cs`. | [hot-paths/pixels-and-images.md](references/hot-paths/pixels-and-images.md) |
## BCL pattern references — the techniques (foundation)
The general .NET fast-API guidance behind the patterns above, with TFM guards.
| Area | Reference |
|---|---|
| Strings & spans | [bcl-patterns/strings-and-spans.md](references/bcl-patterns/strings-and-spans.md) |
| Numerics, SIMD & codegen | [bcl-patterns/numerics-and-simd.md](references/bcl-patterns/numerics-and-simd.md) |
| Memory & buffers | [bcl-patterns/memory-and-buffers.md](references/bcl-patterns/memory-and-buffers.md) |
| Collections & searching | [bcl-patterns/collections.md](references/bcl-patterns/collections.md) |
| Interop & marshalling | [bcl-patterns/interop-and-marshalling.md](references/bcl-patterns/interop-and-marshalling.md) |
---
## Mode selection
| 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](references/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. |
---
## The autonomous workflow (scan → prove → fix → file)
### Phase 0 — Prepare the scan (no native download)
Read the benchmark harness documentation at
[`benchmarks/README.md`](../../../benchmarks/README.md), the template benchmark, and the relevant
proof references; the test project is `tests/SkiaSharp.Tests.Console`. Do not restore local tools
or download pre-built natives during setup, source scanning, or de-duplication. A quiet or
duplicate run ends before either operation.
### Phase 1 — Scan (find ONE candidate)
**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:
```bash
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
```
Use the focus table above to locate the exact reference first, then open that `hot-paths/` file and
its **Where to look** commands. Read only the relevant section, bounded by its next heading; do not
guess a line range or load unrelated references. 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 …`):
```bash
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`).
**1.4 Bootstrap one qualified candidate.** Only after one managed-C# candidate has a citable hot
path/invariant and clears the Phase 1.3 open-item de-dup gate, run this exact command **once per
run**:
```bash
dotnet tool restore && dotnet cake --target=externals-download
```
This is the mandatory bootstrap before any source build, test, or benchmark, not a scan
prerequisite. Do not run either command for a quiet/duplicate candidate, and do not repeat either
command in later phases.
### Phase 2 — Prove it is faster
Follow [measuring.md](references/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.**
### Phase 3 — Fix + prove identical
Write the equivalence test **first** ([measuring.md](references/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
+ neighbours).
**Self-review gate — before the PR** (all must tick, else fix or `noop`):
- [ ] Real, repeatable speedup outside the error bands, ≥2 runs, no alloc regression, realistic workload.
- [ ] Full behaviour parity proven (value/edges/exceptions/ownership/pixels) and the test catches a
deliberately-wrong result.
- [ ] Behaviour unchanged; SkiaSharp still renders identically.
- [ ] Fix in `binding/**`/`source/**` only — no `*.generated.cs`, no `externals/skia/**`.
- [ ] No public signature changed (body/additive overload only).
- [ ] All TFMs handled; no ARM64/x86 SIMD regression; float port keeps the x87 fallback.
- [ ] The matching **Watch out** does not describe what you did; not already covered by an open issue/PR.
### Phase 4 — File the finding, then the linked fix PR
Two linked safe outputs so the finding auto-closes on merge:
- **Issue** (`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).
- **PR** (`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.
- **Labels** — both the issue and PR carry `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.
- If the only real win is native/upstream → the **issue alone** (finding + evidence + proposal).
### Phase 5 — Report
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". Name the
actual checked universe and evidence: for an exhaustive claim, name the bounded query/path and
confirm that every returned result was inspected without truncation; for a sample, say it was
representative and name the files or candidates actually opened. Never infer an exhaustive scan or
aggregate count from a few representative reads. 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.
More agent context in mono/SkiaSharp
26 other files this repository gives its agents.
AGENTS.md
CLAUDE.md
Copilot instructions
Skill
- api-add-review.agents/skills/api-add-review/SKILL.md
- api-docs.agents/skills/api-docs/SKILL.md
- ci-status.agents/skills/ci-status/SKILL.md
- issue-bulk-process.agents/skills/issue-bulk-process/SKILL.md
- issue-fix.agents/skills/issue-fix/SKILL.md
- issue-repro.agents/skills/issue-repro/SKILL.md
- issue-triage.agents/skills/issue-triage/SKILL.md
- memory-leak-fixer.agents/skills/memory-leak-fixer/SKILL.md
- merge-skia-update.agents/skills/merge-skia-update/SKILL.md
- native-dependency-update.agents/skills/native-dependency-update/SKILL.md
- pr-commit-message.agents/skills/pr-commit-message/SKILL.md
- release-audit.agents/skills/release-audit/SKILL.md
- release-branch.agents/skills/release-branch/SKILL.md
- release-milestones.agents/skills/release-milestones/SKILL.md
- release-notes.agents/skills/release-notes/SKILL.md
- release-publish.agents/skills/release-publish/SKILL.md
- review-skia-update.agents/skills/review-skia-update/SKILL.md
- sample-scout.agents/skills/sample-scout/SKILL.md
- security-audit.agents/skills/security-audit/SKILL.md
- skia-analyst.agents/skills/skia-analyst/SKILL.md
- skill-creator.agents/skills/skill-creator/SKILL.md
- update-skia.agents/skills/update-skia/SKILL.md
- agentic-workflows.github/skills/agentic-workflows/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

