Skip to main content

performance-review

Review Rust codebases for performance anti-patterns, with emphasis on async runtimes, client-server networking, memory allocation, serialization, and concurrency. Use when asked to audit, review, or optimize Rust code for performance.

설치로 이동

소스 정보

저장소
borgbase/vykar
최근 소스 활동
2026년 3월 15일 13:33
감지된 SKILL.md 언어
영어
스타
704
포크
20

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
performance-review
description
Review Rust codebases for performance anti-patterns, with emphasis on async runtimes, client-server networking, memory allocation, serialization, and concurrency. Use when asked to audit, review, or optimize Rust code for performance.
# Rust Performance Review Skill You are a Rust performance auditor. Your job is to scan Rust source code and project configuration for known performance anti-patterns and report findings with concrete fix suggestions. ## How to use this skill 1. Start by reading `Cargo.toml` and `Cargo.lock` to understand dependencies, the build profile, target platform, and whether the project uses async (tokio/async-std), networking (hyper/tonic/axum/actix/reqwest), or serialization (serde, serde_json, bincode, etc.). 2. Check Tier 1 (project config) issues first — these are free wins that require no code changes: LTO, codegen-units, target-cpu, panic mode, global allocator on musl. 3. Scan source files for Tier 2 (code pattern) issues using the reference below. Prioritize findings by estimated impact: async executor blocking and missing TCP_NODELAY are nearly always the highest-impact fixes. 4. Report findings grouped by severity (critical / warning / suggestion), with the file path, line reference, the problematic pattern, why it matters, and the recommended fix. Keep explanations terse — link to the relevant section of this reference for details. ## Severity classification - **Critical**: Will cause production incidents under load (executor blocking, unbounded channels, connection-per-request, missing buffered I/O). - **Warning**: Measurable performance loss, 2x+ degradation in affected path (wrong mutex type, SeqCst everywhere, Arc<Mutex<HashMap>>, serde_json for internal APIs, Vec without with_capacity in hot loops, clone abuse). - **Suggestion**: Optimization opportunity, 5–20% improvement potential (LTO, PGO, binary serialization, zero-copy deserialization, SmallVec, vectored I/O, custom allocator). ## Key detection patterns (quick reference) Blocking in async: `std::fs::`, `std::thread::sleep`, `std::net::`, `block_on` inside `async fn`. Networking: Missing `set_nodelay(true)`, raw `TcpStream` without `BufReader`/`BufWriter`, `TcpStream::connect` in request handlers (no pool), `to_socket_addrs()` in async code. Memory: `Vec::new()` + loop `.push()` without `with_capacity`, `.clone()` passed to `&T` parameter, `format!()` inside loops, `Arc<Mutex<HashMap>>`. Serialization: `serde_json::Value` with known schema, `#[serde(untagged)]`, `String` fields in `Deserialize` structs that could borrow. Concurrency: `unbounded_channel()`, `Ordering::SeqCst`, adjacent atomics without cache padding, `tokio::sync::Mutex` with no `.await` inside lock scope. Config: Missing `lto`, `codegen-units > 1`, no `target-cpu=native`, musl without `#[global_allocator]`. ## Full reference The sections below contain the complete catalog of ~50 anti-patterns with detection strategies, impact analysis, fixes, and real-world case studies. Consult them for details when reporting findings. --- ## 1. Async runtime pitfalls that silently kill throughput ### Blocking the executor The single most common Rust async performance bug is running blocking operations on tokio worker threads. Every call to `std::thread::sleep`, `std::fs::read`, synchronous DNS resolution, or CPU-heavy computation (bcrypt, compression) prevents the cooperative scheduler from switching tasks. Alice Ryhl (Tokio maintainer) states that **async code should never spend more than 10–100 microseconds without reaching an `.await`**. With 4 runtime threads and 100ms blocking per request, maximum throughput collapses to 40 req/s — with no errors or panics, just silent latency degradation. **Detection signals:** Grep for `std::thread::sleep`, `std::fs::`, `std::net::TcpStream`, `std::io::Read`, `std::io::Write`, and `block_on` inside `async fn` or `async` blocks. Check for synchronous database drivers (e.g., `diesel` without `spawn_blocking`). The `tokio-console` tool measures task poll durations, and the `hud` crate provides zero-instrumentation blocking detection. **Fix:** Use `tokio::task::spawn_blocking` for blocking I/O, `tokio::fs` for file operations, `tokio::time::sleep` for delays, and offload CPU-heavy work to `rayon` via a oneshot channel bridge. ### Sequential `.await` chains destroying concurrency Rust futures are lazy — they do nothing until polled. Consecutive `let a = fetch_a().await; let b = fetch_b().await;` runs operations serially even when they're independent, making total time equal to the sum rather than the maximum. **Detection:** Look for multiple consecutive `let x = something().await;` lines where operations don't depend on each other's results. An AST check can identify independent await expressions in sequence. **Fix:** Use `tokio::join!` for a fixed set of futures, `JoinSet` for dynamic collections, or `FuturesUnordered` for stream-based processing. ### Wrong mutex type selection Using `tokio::sync::Mutex` when `std::sync::Mutex` suffices incurs **~2x overhead**. The async mutex is only needed when the lock must be held across `.await` points. Conversely, holding a `std::sync::MutexGuard` across an `.await` causes compile errors on multi-threaded runtimes (the guard is `!Send`) or deadlocks on single-threaded runtimes. **Detection:** Flag `tokio::sync::Mutex` where no `.await` occurs between lock acquisition and release. Flag `std::sync::Mutex` guards whose scope spans an `.await` point. Pattern: `.lock()` followed by `.await` without an intervening `drop()` or scope boundary. ### Large futures and stack pressure Async functions compile into state machines where every variable held across an `.await` becomes a field in the future's struct. Large stack arrays, deeply nested async calls, or many local variables can create futures of **tens to hundreds of kilobytes** — the Fuchsia team observed 400KB single futures. Since Tokio 1.41.0, futures larger than 16KB (release) passed to `tokio::spawn` are automatically boxed, but the allocation cost remains. **Detection:** Grep for large array declarations `[T; N]` where N is large inside async functions. Use `std::mem::size_of_val(&future)` to measure. `tokio-console` reports `size.bytes` per task. ### Cancellation safety in `select!` When a `tokio::select!` branch completes, other branches' futures are dropped. Operations like `read_exact`, `write_all`, and `Mutex::lock` are **not cancellation-safe** — partial progress is silently lost, causing data corruption or starvation. **Detection:** Flag `tokio::select!` or `futures::select!` in loops containing `read_exact`, `write_all`, `BufReader::read_line`, or other non-cancellation-safe methods. The Tokio docs annotate each method's cancellation safety status. --- ## 2. Networking configuration mistakes with outsized latency impact ### TCP_NODELAY: the number-one networking fix By default, TCP uses Nagle's algorithm, which buffers small writes and delays sending until a full segment accumulates or an ACK arrives. For RPC and interactive protocols, this adds **40–200ms of latency per request**. Axum issue #2521 documented massive latency regressions because `TCP_NODELAY` wasn't set by default. The community consensus is emphatic: "It's always TCP_NODELAY." **Detection:** Grep for every `TcpStream::connect` and `listener.accept()` call and verify that `set_nodelay(true)` follows. Check hyper/axum/tonic configuration for TCP_NODELAY settings. ### Missing buffered I/O on network streams Every `read()` or `write()` on a raw `TcpStream` triggers a system call. The ScyllaDB Rust driver team discovered this was their **#1 performance bottleneck** — their driver issued at least one syscall per query, causing 2x CPU usage compared to their C++ driver. Wrapping streams with `BufReader`/`BufWriter` is a drop-in fix that batches operations into 8KB chunks by default. **Detection:** Flag `TcpStream` used directly for read/write without `BufReader`/`BufWriter` wrapping. Check flamegraphs for excessive time in `sendmsg`/`recvmsg` syscalls. ### Connection pool exhaustion and DNS blocking Without connection pooling, each request opens a new TCP connection plus TLS handshake plus authentication — **50–150ms overhead per connection**. Even with pools, exhaustion occurs when pools are undersized or connections are held across slow operations. Standard library DNS resolution (`ToSocketAddrs::to_socket_addrs()`) is synchronous and will block the async runtime. **Detection:** Flag `TcpStream::connect` in request handlers (should use a pool). Flag `to_socket_addrs()` in async code. Flag `pool.get()` where the resulting connection spans unrelated `.await` calls. Verify reqwest clients configure `pool_idle_timeout` and `pool_max_idle_per_host`. ### HTTP/2 flow control and window sizing Default HTTP/2 flow control windows (typically 64KB) throttle large message transfers. Tonic issue #569 documented that transferring 0.1GB took over a minute with default settings because flow control required many round-trips for window updates. Setting `http2_initial_stream_window_size`, `http2_initial_connection_window_size`, and enabling `http2_adaptive_window` resolves this. **Detection:** Flag tonic/hyper servers transferring large payloads without custom HTTP/2 window configuration. Check for `SETTINGS_MAX_CONCURRENT_STREAMS` configuration to prevent resource exhaustion. --- ## 3. Memory allocation patterns that compound under load ### Heap allocation in hot paths Each heap allocation involves acquiring a global lock, non-trivial bookkeeping, and possibly a system call. The Rust Performance Book states that **reducing allocations by 10 per million instructions can yield measurable improvements (~1%)**. Common offenders include `Vec::new()`, `String::new()`, `Box::new()`, and `format!()` inside loops. **Detection:** Flag allocating constructors (`Vec::new()`, `String::new()`, `format!()`, `Box::new()`) inside `loop`, `for`, and `while` blocks. Use DHAT profiler to identify hot allocation sites. **Fix:** Reuse buffers across calls by passing `&mut String` or `&mut Vec<T>` parameters. Use `write!` to a reusable buffer instead of `format!()`. Pre-allocate with `Vec::with_capacity()` when the size is known. ### Vec reallocations from missing `with_capacity` Vec's growth strategy (0→4→8→16→32→64...) means pushing 1000 items one-by-one causes ~10 reallocations, each copying all existing elements. A rustc PR showed that adjusting Vec's initial growth reduced allocations by **10%+ and sped up benchmarks by up to 4%**. **Detection:** Flag `Vec::new()` or `vec![]` followed by `.push()` in a loop with a known or estimable bound. Same for `String::with_capacity()` and `HashMap::with_capacity()`. ### Clone abuse and alternatives Cloning heap-allocated data (Vec, String, HashMap) copies all data. The Rust Design Patterns book explicitly identifies "clone to satisfy the borrow checker" as an anti-pattern. Key alternatives: - **`Cow<'a, str>`** for conditional ownership — avoids allocation when no modification is needed - **`Arc<T>`** for shared ownership — clone only increments a reference count - **`clone_from(&b)`** instead of `a = b.clone()` — reuses `a`'s existing heap allocation - **`as_deref()`** for `Option<String>` → `Option<&str>` conversion **Detection:** Flag `.clone()` calls where the value is immediately passed to a function taking `&T`. Clippy's `redundant_clone` lint catches some cases. Flag functions returning `String` that sometimes return the input unchanged (should use `Cow`). ### SmallVec for short-lived small collections `Vec` always heap-allocates when non-empty. For collections that typically hold fewer than 8–16 elements, `SmallVec<[T; N]>` stores them inline on the stack. The rustc compiler uses SmallVec extensively — PRs #50565 and #55383 showed **measurable compilation speed improvements**. **Detection:** Flag `Vec<T>` in struct fields where profiling shows vectors typically contain fewer than 8–16 elements. Particularly valuable for compiler-like workloads, parsers, and intermediate results. ### Arc\<Mutex\<HashMap\>\> contention Coarse-grained locking on a single `Arc<Mutex<HashMap>>` serializes all operations — even reads on different keys block each other. Alternatives ordered by use case: - **`DashMap`**: Sharded `RwLock<HashMap>` with per-bucket locking. Best general-purpose concurrent map. - **`scc::HashMap`**: Fine-grained bucket locks with epoch-based GC. Best for write-heavy workloads. - **`papaya`**: Lock-free reads (RCU-style). Best for read-heavy workloads (no reader-side locking at all). **Detection:** Flag `Arc<Mutex<HashMap<_,_>>>` and `Arc<RwLock<HashMap<_,_>>>` — suggest sharded alternatives. --- ## 4. Serialization choices that multiply latency ### Typed deserialization vs `serde_json::Value` `serde_json::Value` creates a tree of heap-allocated nodes — every string, array, and object is a separate allocation. Typed deserialization into a struct is **1.5–2x faster** (550–710 MB/s vs 300–420 MB/s in json-benchmark). **Detection:** Flag `serde_json::from_str` or `from_reader` with target type `serde_json::Value` when the schema is known. Pattern: deserialization to `Value` followed by field access with string keys like `v["name"]`. ### Zero-copy deserialization with `#[serde(borrow)]` Every `String` field in a `#[derive(Deserialize)]` struct allocates heap memory and copies bytes from the input. Using `&'a str` with `#[serde(borrow)]` borrows directly from the input buffer — **~2x faster** for string-heavy payloads. The `zerovec` crate enables zero-heap-allocation deserialization for vectors. **Detection:** Flag structs with `String` fields used with `Deserialize` where the input lifetime outlives usage. Pattern: `#[derive(Deserialize)]` structs with `String` fields that could be `&str` or `Cow<str>`. **Constraint:** Only works with `from_str`/`from_slice`, not `from_reader`. JSON escape sequences force allocation. ### Binary formats vs JSON for internal APIs JSON is **5–15x slower** than binary formats for serialization. Key benchmarks from `rust_serialization_benchmark`: | Format | Relative speed | Best for | |--------|---------------|----------| | **bincode** | ~40ns ser, ~100ns deser | Fastest general-purpose | | **rkyv** | ~21ns zero-copy deser | Total zero-copy, no parsing step | | **bitcode** | Best combined scores | Newest, excellent compression | | **simd-json** | 2–3x faster than serde_json | JSON-compatible with SIMD | | **postcard** | ~60ns ser, ~180ns deser | Embedded-friendly, compact | | **serde_json** | ~250ns ser, ~500ns deser | Human-readable only | **Detection:** Flag `serde_json` usage in non-user-facing code paths (internal APIs, caches, IPC). Suggest binary formats for machine-to-machine communication. ### `#[serde(untagged)]` enum performance trap Serde's official docs warn that untagged enums try each variant in order, deserializing and backtracking on failure. The input may be parsed **multiple times**. Use `#[serde(tag = "type")]` (internally tagged) or `#[serde(tag = "type", content = "data")]` (adjacently tagged) instead. **Detection:** Flag `#[serde(untagged)]` attribute on enums, especially with many variants in hot deserialization paths. --- ## 5. Concurrency primitives that become bottlenecks ### Unbounded channels: a ticking OOM bomb Unbounded channels (`tokio::sync::mpsc::unbounded_channel`, `crossbeam::channel::unbounded`) have no backpressure. If producers outpace consumers, memory grows without bound until OOM. Tokio issue #4321 documents that even after the spike clears, **memory is never deallocated** from blocks allocated during the spike. Community consensus: "nobody likes unboundedness and most have experienced production outages because of it." **Detection:** Flag any usage of `unbounded_channel()` or `unbounded()`. This is a hard rule — always use bounded channels in production. ### Atomic ordering: SeqCst is almost never needed
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기