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 (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.
Golden rules (non-negotiable)
- One optimization per run. Pick the single strongest candidate; a perf PR is only reviewable
as one before/after with one benchmark.
- Two proofs, always — speed AND correctness (details in 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.
- 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.
- Never weaken, skip, mute,
[Obsolete]-hide, or delete a test. If a correctness test goes red,
fix the change, not the test.
- 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.
- ABI stability. Change method bodies or add overloads; never change/remove a public
signature. (#4241 changed only bodies; #4345 added
ReadOnlySpan<char> overloads.)
- 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.
- 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.
- 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
- Decide if it's worth it. 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.
- Reuse before you build. repo-helpers.md — a shared helper
(
Utils.RentArray, RentHandlesArray, SKString) or the native oracle may already fit.
- Route from the signal. signals.md maps what the code does → the
hot-path / bcl-pattern reference that covers it.
- Prove it. 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. 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 |
| 1 |
Color parse / convert |
Parse, format, and conversion helpers in binding/SkiaSharp/ and binding/HarfBuzzSharp/. |
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 |
| 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 |
| 4 |
Pixels & images |
Bulk pixel/scanline paths and array materialization in SKBitmap.cs, SKPixmap.cs, and SKImage.cs. |
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 |
| 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 |
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, 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, 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:
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 …):
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:
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 §"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 §"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):
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.
1---2name: performance-fixer3description: 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`.4---56# Performance Fixer78Proactively **find** and **fix** performance problems in SkiaSharp — a thin managed wrapper over9native Skia, so its recurring, high-impact family is **the managed layer's own overhead between the10caller and Skia**: a P/Invoke transition paid for math that is a few float ops, an allocation on a11hot parse/convert path, a native lookup redone on every getter, per-element marshalling in a loop.12This is *not* about making Skia's C++ rasterizer faster (that is upstream); it is about removing the13tax the C# layer imposes. Every fix is **measured** (a benchmark) and **behaviour-preserving** (an14equivalence test).1516**Scope: managed C# only** — `binding/**` and `source/**`. Everything under `externals/skia/**`17(including our C shim) is upstream Skia: out of scope to edit or build, though you **may read** the18pinned source to verify an invariant. Every candidate must be provable and fixable from C#.1920Read [`references/decision-framework.md`](references/decision-framework.md) (is it worth it? the21impact×complexity rubric + the two-proof gate) and [`references/measuring.md`](references/measuring.md)22(how to prove faster **and** identical) first — they are the model this skill runs on. Background on23the interop boundary is in [`documentation/dev/memory-management.md`](../../../documentation/dev/memory-management.md)24and [`documentation/dev/architecture.md`](../../../documentation/dev/architecture.md).2526## Golden rules (non-negotiable)27281. **One optimization per run.** Pick the single strongest candidate; a perf PR is only reviewable29 as one before/after with one benchmark.302. **Two proofs, always — speed AND correctness** (details in [measuring.md](references/measuring.md)):31 a BenchmarkDotNet `New` vs `Old` shows a **meaningful, repeatable** speedup with no allocation32 regression; an equivalence test proves the result is **identical to the original/native path**33 (bit-exact for numeric ports) across normal *and* edge inputs. No speedup ⇒ nothing to fix. Any34 behaviour change ⇒ reject — a faster answer that differs from Skia is a **rendering regression**.353. **Never trade correctness for speed.** No "approximation", no dropped edge case36 (NaN/±0/Inf/degenerate/overflow), no changed rounding, no skipped validation. If the only way37 faster changes what the method returns, **stand down**.384. **Never weaken, skip, mute, `[Obsolete]`-hide, or delete a test.** If a correctness test goes red,39 fix the change, not the test.405. **Never edit generated files or upstream Skia.** `*.generated.cs` and `externals/skia/**` are41 off-limits to edit/build. You **may READ** the pinned Skia C++ (fetch at the submodule's pinned42 commit and cite it) to verify an algorithm or pointer-stability invariant.436. **ABI stability.** Change method **bodies** or add **overloads**; never change/remove a public44 signature. (#4241 changed only bodies; #4345 added `ReadOnlySpan<char>` overloads.)457. **Float determinism across runtimes.** A managed port of native float math is bit-exact only on46 SSE2/NEON runtimes; **x86 .NET Framework (x87) diverges** — any float port must keep a native47 fallback there (a `RuntimeInformation`-gated `static readonly bool`, as #4241 did). Never ship a48 float port without it.498. **Honest, numeric scope note.** Report the **actual measured numbers** (Mean/Error/StdDev,50 allocations, ratio) on named hardware/TFM; say what is *empirically measured* vs *statically51 reasoned*, plus ABI impact. Never claim a speedup you did not measure.529. **Finding nothing is the expected outcome.** SkiaSharp is mature; most obvious overhead is already53 optimized. Most runs should end with **no candidate**. A 2% win on a synthetic micro-loop no real54 caller hits is **not** a finding. A quiet run is a first-class success — emit a `noop`.5556## How to use this skill57581. **Decide if it's worth it.** [decision-framework.md](references/decision-framework.md): be59 aggressive with low-complexity wins on hot paths; reserve high-complexity (native-math ports,60 SIMD, caching) for measured cases. Confirm a **realistic hot caller** first.612. **Reuse before you build.** [repo-helpers.md](references/repo-helpers.md) — a shared helper62 (`Utils.RentArray`, `RentHandlesArray`, `SKString`) or the native oracle may already fit.633. **Route from the signal.** [signals.md](references/signals.md) maps *what the code does* → the64 hot-path / bcl-pattern reference that covers it.654. **Prove it.** [measuring.md](references/measuring.md) — both proofs, against this repo's harness.6667## The cheap wins (apply by default on hot paths)6869Low complexity, high impact. Prefer them whenever you write or touch hot-path code.7071- Prefer the span/`Try*` overload over the allocating one; add a `ReadOnlySpan<char>` overload where72 only the `string`/`T[]` one exists (additive, ABI-safe).73- Pre-size and pool: give collections a `capacity`, rent from `Utils.RentArray`/`ArrayPool`.74- `stackalloc` a small, **bounded** buffer instead of allocating (cap the size; never in a loop).75- Cache a stable native wrapper across calls when the four preconditions hold (pointer identity,76 lifetime, disposal invalidation, thread model).77- Size the specialized type: `SearchValues<T>` for repeated set search, `FrozenDictionary` for78 build-once maps.79- Let the JIT help: `sealed` internal types, `[MethodImpl(AggressiveInlining)]` on trivial wrappers,80 `in`/`ref readonly` on large structs (internal / new overloads only), avoid LINQ/boxing in loops.8182## Be cautious with (measure first, isolate, keep all TFMs safe)8384High complexity — apply only on a **proven** hot path, behind a clean API, with the two proofs. Even85when you recommend the simpler option, report the faster high-complexity one and its tradeoff.8687- Porting native float math to managed C# (bit-exact + the x87 fallback).88- Manual SIMD / `Vector128`/`Vector256` (ARM64 NEON `Vector256` was **5.7–6.5× slower** in #4241).89- `unsafe`, raw pointers, `MemoryMarshal.Cast`/`Unsafe.As` reinterpretation.90- Any change to the `HandleDictionary` locking discipline.9192## Hot-path references — where the wins live (primary)9394Route here from [signals.md](references/signals.md). Start with the selected `FOCUS` row, open only95its linked reference, then use that file's full *Where to look* commands. Each reference also has96the slow→fast, watch-out, and real PR.9798| `FOCUS` | SkiaSharp area | Where to look | Reference |99|---:|---|---|---|100| 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) |101| 1 | Color parse / convert | Parse, format, and conversion helpers in `binding/SkiaSharp/` and `binding/HarfBuzzSharp/`. | [hot-paths/color.md](references/hot-paths/color.md) |102| 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) |103| 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) |104| 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) |105106## BCL pattern references — the techniques (foundation)107108The general .NET fast-API guidance behind the patterns above, with TFM guards.109110| Area | Reference |111|---|---|112| Strings & spans | [bcl-patterns/strings-and-spans.md](references/bcl-patterns/strings-and-spans.md) |113| Numerics, SIMD & codegen | [bcl-patterns/numerics-and-simd.md](references/bcl-patterns/numerics-and-simd.md) |114| Memory & buffers | [bcl-patterns/memory-and-buffers.md](references/bcl-patterns/memory-and-buffers.md) |115| Collections & searching | [bcl-patterns/collections.md](references/bcl-patterns/collections.md) |116| Interop & marshalling | [bcl-patterns/interop-and-marshalling.md](references/bcl-patterns/interop-and-marshalling.md) |117118---119120## Mode selection121122| You were asked to… | Do this |123|---|---|124| 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 #…`). |125| 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. |126| 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. |127128---129130## The autonomous workflow (scan → prove → fix → file)131132### Phase 0 — Prepare the scan (no native download)133Read the benchmark harness documentation at134[`benchmarks/README.md`](../../../benchmarks/README.md), the template benchmark, and the relevant135proof references; the test project is `tests/SkiaSharp.Tests.Console`. Do not restore local tools136or download pre-built natives during setup, source scanning, or de-duplication. A quiet or137duplicate run ends before either operation.138139### Phase 1 — Scan (find ONE candidate)140**1.1 Pick a focus area (round-robin).** If the run supplies an explicit focus area (a bare number1410–4), use it and skip rotation. Otherwise rotate over the **5 hot-path areas** so consecutive runs142differ:143```bash144DOY=$(date -u +%j); HOUR=$(date -u +%H) # zero-padded day-of-year + hour145FOCUS=$(( (10#$DOY * 24 + 10#$HOUR) % 5 )) # 10# forces base-10; 0..4146echo "focus area: $FOCUS" # 0 geometry-math · 1 color · 2 handles-and-collections · 3 text-and-fonts · 4 pixels-and-images147```148Use the focus table above to locate the exact reference first, then open that `hot-paths/` file and149its **Where to look** commands. Read only the relevant section, bounded by its next heading; do not150guess a line range or load unrelated references. Widen to a neighbour only if it's exhausted.151152**1.2 Establish the hot path and cost** — with `file:line` citations: the realistic caller and how153often it runs; the concrete overhead (which the reference names); and the invariant that makes the154fast path *still correct*. If you can't name that invariant, drop it. Skip anything already optimized155(the references list the hardened spots).156157**1.3 De-dup** against open issues/PRs (search the `[performance]` prefix **and** the specific158type/API name — real perf work is often `perf(...)`/`Optimize …`):159```bash160gh issue list --repo "$GITHUB_REPOSITORY" --search '"[performance]" in:title' --state open --json number,title161gh pr list --repo "$GITHUB_REPOSITORY" --search 'SKMatrix in:title' --state open --json number,title162```163Respect in-flight work (#4241 SKMatrix, #4276/#3699 bench CI, #3489 CopyTo, #4182 dict sizing,164#3033 DrawShapedText). Pick the ONE strongest candidate; if none convinces, **stop** (`noop`).165166**1.4 Bootstrap one qualified candidate.** Only after one managed-C# candidate has a citable hot167path/invariant and clears the Phase 1.3 open-item de-dup gate, run this exact command **once per168run**:169```bash170dotnet tool restore && dotnet cake --target=externals-download171```172This is the mandatory bootstrap before any source build, test, or benchmark, not a scan173prerequisite. Do not run either command for a quiet/duplicate candidate, and do not repeat either174command in later phases.175176### Phase 2 — Prove it is faster177Follow [measuring.md](references/measuring.md) §"Proof 1": a `New` vs `Old` benchmark in one process,178`[MemoryDiagnoser]`, realistic workload, statistical rigor (Mean/Error/StdDev, ≥2 runs, no alloc179regression, no regression on any real shape). **No measurable/repeatable win ⇒ not a finding.**180181### Phase 3 — Fix + prove identical182Write the equivalence test **first** ([measuring.md](references/measuring.md) §"Proof 2") — full183behaviour parity (return value bit-exact for numeric ports; edge inputs; exceptions/validation;184ownership/`GC.KeepAlive`; rendered pixels), confirmed to catch a deliberately-wrong result. Then185implement the minimal fix using the matching hot-path + bcl-pattern references, honouring that186family's **Watch out** and **all TFMs** (guard newer APIs; a float port keeps the x87 fallback).187Confirm: identical (equivalence passes), faster (benchmark holds), no regressions (type's test class188+ neighbours).189190**Self-review gate — before the PR** (all must tick, else fix or `noop`):191- [ ] Real, repeatable speedup outside the error bands, ≥2 runs, no alloc regression, realistic workload.192- [ ] Full behaviour parity proven (value/edges/exceptions/ownership/pixels) and the test catches a193 deliberately-wrong result.194- [ ] Behaviour unchanged; SkiaSharp still renders identically.195- [ ] Fix in `binding/**`/`source/**` only — no `*.generated.cs`, no `externals/skia/**`.196- [ ] No public signature changed (body/additive overload only).197- [ ] All TFMs handled; no ARM64/x86 SIMD regression; float port keeps the x87 fallback.198- [ ] The matching **Watch out** does not describe what you did; not already covered by an open issue/PR.199200### Phase 4 — File the finding, then the linked fix PR201Two linked safe outputs so the finding auto-closes on merge:202- **Issue** (`create_issue`, `temporary_id` like `aw_perf1`) — the **hot path + measured cost**203 (family, `file:line`, the realistic caller, the Phase 2 benchmark table, the scope note).204- **PR** (`create_pull_request`, draft, branch `dev/perf-<desc>`) — the fix (what changed + the205 invariant that keeps it correct), **proof faster** (benchmark table + command), **proof identical**206 (the equivalence test + what edges it covers + that it catches a wrong result), and `Fixes #aw_perf1`207 on its own line.208- **Labels** — both the issue and PR carry `tenet/performance`; add the matching **`perf/*`209 sub-type** chosen by the dominant, measured driver of the win (a removed P/Invoke → `perf/interop`,210 removed managed allocations → `perf/allocations`, else `perf/rendering`/`perf/throughput`/211 `perf/startup`/`perf/memory-leak`/`perf/size`). Canonical taxonomy:212 `.agents/skills/issue-triage/references/labels.md`. Usually one sub-type. When run from the agentic213 workflow, its guardrail 8 restates this.214- If the only real win is native/upstream → the **issue alone** (finding + evidence + proposal).215216### Phase 5 — Report217Short summary: area, candidate (`file:line`), benchmark result (New vs Old, ratio, allocations),218equivalence coverage, and the issue + PR links — or "no convincing candidate this run". Name the219actual checked universe and evidence: for an exhaustive claim, name the bounded query/path and220confirm that every returned result was inspected without truncation; for a sample, say it was221representative and name the files or candidates actually opened. Never infer an exhaustive scan or222aggregate count from a few representative reads. End with the right safe output: the **issue + PR223pair**, the **issue alone** (native/upstream), or a single **`noop`** (quiet/dry run). Never finish224with no safe output.