---
name: rust-perf-review
description: Performance review for Rust projects. Use when the user asks to "review performance", "perf review", "find bottlenecks", "optimise the Rust code", "profile the project", or "check for unnecessary allocations".
version: 1.0.0
allowed-tools: [Read, Bash, Glob, Grep]
---

# Rust Performance Review

Conduct a structured performance review of a Rust codebase. Find real, measurable inefficiencies — not theoretical micro-optimisations. Prioritise findings by likely impact.

## Review Process

1. Read `Cargo.toml` — dependencies, features, release profile settings.
2. Identify the hot paths: entry points, the main processing loop, any I/O or compute-heavy paths.
3. Read the identified hot path files in full.
4. Apply each dimension below to those files.
5. Report findings grouped by severity.

---

## Review Dimensions

### 1. Allocation Patterns

Grep for `.clone()` — for each call, determine whether the clone is required or a borrow would suffice. Common false necessities: cloning into a function that only reads, cloning before passing to a function that takes ownership but where the original is not used again.

Look for:

- **`String` parameters that should be `&str`** — function parameters typed `String` that are never mutated or stored long-term should be `&str` or `impl AsRef<str>`.
- **`Vec::new()` inside a loop** — allocation on every iteration. Hoist outside the loop and reuse with `.clear()`.
- **`.collect::<Vec<_>>()` followed immediately by iteration** — remove the collect and keep the iterator chain.
- **`format!` for temporary strings** — always allocates. If used only for a log line or error, consider `write!` to a pre-allocated buffer or a `Display` impl.
- **`Box<dyn Trait>` in hot paths** — heap indirection and dynamic dispatch in tight loops. Prefer generics and monomorphisation unless the type set is genuinely open at runtime.
- **Repeated `.to_string()` / `.to_owned()` on the same value** — hoist to a single allocation.

### 2. Data Structures

Evaluate whether the chosen container matches the access pattern:

- `HashMap` with integer keys → consider a plain `Vec` indexed directly or a small-map optimisation.
- `HashMap` with a small, fixed set of string keys → `match` or `phf` crate eliminates hashing entirely.
- `Vec` used as a set (linear `.contains()` calls) → `HashSet`, or sort+binary search if the set is built once.
- `Vec<Vec<T>>` (jagged 2D data) → flat `Vec<T>` with manual index arithmetic for better cache locality.
- Nested `Option<Option<T>>` or `Option<Result<T, E>>` → flatten the type to remove branching.
- Struct field ordering — Rust preserves declaration order. Group hot fields together at the top of the struct to improve cache-line utilisation.

### 3. Iterator Chains

- `.collect()` in the middle of a chain that is then immediately re-iterated — eliminate the intermediate collection.
- Indexing two slices with `[i]` inside a loop — use `.iter().zip()` to allow bounds-check elimination.
- `.enumerate()` where the index is unused — drop it.
- `.clone()` inside `.map()` on an iterator of references — check if the downstream consumer can work with the reference directly.

### 4. Cloning and Ownership

Beyond allocations, check the overall ownership strategy:

- Functions that take `T` by value but only read it — should take `&T`.
- `Arc<T>` or `Rc<T>` used where single ownership would suffice — reference counting has overhead on every clone and drop.
- `Arc<Mutex<T>>` in a hot path — check whether the lock scope can be narrowed, the data restructured, or `RwLock` used for read-heavy access.
- Cloning a large struct to pass to a thread when only a field is needed — clone only the field, or wrap the relevant data in `Arc`.

### 5. I/O

- **Unbuffered reads/writes** — direct `File::read` / `File::write` calls without `BufReader` / `BufWriter`. Wrap with buffering.
- **Reading an entire file into memory** — `fs::read_to_string` for large files. Prefer streaming with `BufReader::lines()` or a chunked read.
- **Synchronous I/O inside an async runtime** — `std::fs` or `reqwest::blocking` called from an async context blocks the executor thread. Use `tokio::fs` or `spawn_blocking`.
- **Repeated stat/open calls on the same path** — cache the result.

### 6. Serialisation

- **`serde_json` on large payloads in a hot path** — JSON is slow to parse. Consider `bincode` or `rmp-serde` (MessagePack) for internal formats.
- **Deserialising into owned `String` fields** — if the data is only read, deserialise into `&'de str` with `#[serde(borrow)]`.
- **Re-parsing static config on every call** — parse once at startup, store in a `OnceLock` or pass through the call stack.

### 7. Parallelism

- Is `rayon` available? `.par_iter()` on independent work items is the lowest-effort CPU parallelism win.
- Sequential processing of independent files, requests, or chunks — a `rayon::scope` or `thread::spawn` + join trivially parallelises this.
- Over-parallelism — spawning a thread per small item creates more scheduling overhead than the work saves. Batch items before parallelising.
- `Mutex` contention — if multiple threads contend on the same lock, consider sharding the data structure or using lock-free alternatives (`DashMap`, `flume`, channels).

### 8. Async

- **Spawning a task per small unit of work** — task creation and scheduling has overhead. Batch work before spawning.
- **`await`ing inside a loop sequentially** — if the iterations are independent, use `futures::future::join_all` or `FuturesUnordered`.
- **Holding a `Mutex` guard across an `.await`** — blocks the thread; use `tokio::sync::Mutex` or restructure to drop the guard before awaiting.
- **`async fn` in a trait via `async-trait`** — each call boxes the future. Where performance matters, consider `impl Future` return types manually.

### 9. Compiler and Build Settings

Check `Cargo.toml` `[profile.release]` against these recommended settings:

```toml
[profile.release]
opt-level = 3       # default; ensure it has not been accidentally lowered
lto = "thin"        # enables cross-crate inlining; use "fat" for maximum at cost of link time
codegen-units = 1   # disables parallel codegen for better inlining
panic = "abort"     # eliminates unwinding machinery; smaller binary, lower overhead
```

Also check:

- Is `debug = true` set under `[profile.release]`? Inflates binary size.
- Are any `[profile.release.package.*]` overrides lowering opt-level for hot dependencies?
- Is `RUSTFLAGS="-C target-cpu=native"` set for local benchmarking? Without it, SIMD code generation is conservative.
- Are dev-only dependencies accidentally enabled in release builds via unconditional `features`?

### 10. Profiling Guidance

If the location of the bottleneck is not obvious from static analysis, recommend the appropriate tool before spending time on speculative fixes:

| Goal | Tool |
|---|---|
| Find which functions consume the most CPU time | `cargo flamegraph` (wraps `perf` on Linux, `dtrace` on macOS) |
| Measure wall time of specific functions | `criterion` micro-benchmarks |
| Find heap allocations and their call sites | `heaptrack` (Linux) or `dhat-rs` crate |
| Inspect generated assembly | `cargo-asm` or Compiler Explorer |
| Check for missed auto-vectorisation | `RUSTFLAGS="-C remark=loop-vectorize" cargo build --release 2>&1 \| grep missed` |
| Measure cache misses and branch mispredictions | `perf stat -e cache-misses,branch-misses ./binary` |

---

## Output Format

Group findings into:

**Critical** — confirmed inefficiencies with high likely impact (e.g., unbuffered I/O, clone in a hot loop, missing `lto`)

**Likely issues** — patterns almost certainly wasteful but where magnitude depends on runtime behaviour

**Worth investigating** — candidates that may or may not matter; suggest a specific profiling step to confirm

**Build settings** — a diff of current `[profile.release]` vs recommended

Close with a **suggested profiling order**: which tool to run first and what specific output to look for, so the user can confirm the top finding before investing in fixes.
