| name | performance |
| description | Optimize Ruby Fast LSP performance, profile latency and memory, benchmark changes, and make performance-critical decisions. |
Performance Skill
Use this skill when optimizing code, profiling performance, or making performance-critical decisions in the Ruby Fast LSP project. Provides guidance on benchmarking, profiling, and optimization patterns. Triggers: performance, optimization, slow, profiling, benchmark, memory, latency, speed.
Performance Philosophy
Priority Order
- Correctness first - Wrong fast code is useless
- Measure before optimizing - No premature optimization
- Optimize the right thing - Profile to find bottlenecks
- Simple optimizations first - Low-hanging fruit
Resource Priority (Slowest First)
- Disk I/O - Milliseconds per operation
- Network - Variable, often slow
- Memory allocation - Microseconds
- CPU computation - Nanoseconds
Optimize slowest resources first, adjusted for frequency.
Profiling Commands
CPU Profiling with Flamegraph
cargo install flamegraph
sudo cargo flamegraph --bin ruby-fast-lsp -- /path/to/project
open flamegraph.svg
Memory Profiling with dhat
[dev-dependencies]
dhat = "0.3"
static ALLOC: dhat::Alloc = dhat::Alloc;
fn main() {
let _profiler = dhat::Profiler::new_heap();
// ... rest of main
}
cargo run --features dhat-heap
Benchmark with Criterion
[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "indexing"
harness = false
use criterion::{criterion_group, criterion_main, Criterion};
fn bench_index_file(c: &mut Criterion) {
let content = include_str!("../fixtures/large_file.rb");
c.bench_function("index_1000_line_file", |b| {
b.iter(|| {
index_content(content)
})
});
}
criterion_group!(benches, bench_index_file);
criterion_main!(benches);
cargo bench
Key Performance Patterns
1. String Interning with Ustr
For frequently compared/stored strings:
use ustr::Ustr;
struct Symbol {
name: Ustr,
}
let a: Ustr = "MyClass".into();
let b: Ustr = "MyClass".into();
assert!(a == b);
struct Symbol {
name: String,
}
When to use Ustr:
- FQN names (constants, methods)
- File paths (repeated lookups)
- Symbol names in index
2. Pre-allocation
Avoid repeated allocations in loops:
let mut results = Vec::with_capacity(expected_count);
for item in items {
results.push(process(item));
}
let mut results = Vec::new();
for item in items {
results.push(process(item));
}
3. Avoid Cloning in Hot Paths
fn process_symbols(symbols: &[Symbol]) {
for symbol in symbols {
analyze(symbol);
}
}
fn process_symbols(symbols: Vec<Symbol>) {
for symbol in symbols {
analyze(&symbol);
}
}
4. Use Slices Over Vectors
fn find_in(haystack: &[Symbol], needle: &str) -> Option<&Symbol>
fn find_in(haystack: Vec<Symbol>, needle: &str) -> Option<Symbol>
5. Batch Operations
fn index_files(&mut self, files: &[PathBuf]) {
let mut batch = Vec::with_capacity(files.len());
for file in files {
batch.push(self.process_file(file));
}
self.index.insert_batch(batch);
}
fn index_files(&mut self, files: &[PathBuf]) {
for file in files {
let processed = self.process_file(file);
self.index.insert(processed);
}
}
6. Lazy Evaluation
fn get_type_info(&self) -> Option<TypeInfo> {
self.cached_type.get_or_init(|| {
expensive_type_computation()
}).clone()
}
fn get_type_info(&self) -> Option<TypeInfo> {
expensive_type_computation()
}
Data Structure Selection
For Lookups
| Need | Structure | Lookup | Insert |
|---|
| Key-value by string | HashMap<Ustr, V> | O(1) | O(1) |
| Sorted by key | BTreeMap<K, V> | O(log n) | O(log n) |
| Prefix matching | Trie | O(k) | O(k) |
| Set membership | HashSet<Ustr> | O(1) | O(1) |
For Collections
| Need | Structure | Notes |
|---|
| Ordered, indexed | Vec<T> | Best for iteration |
| LIFO | Vec<T> | Use as stack |
| FIFO | VecDeque<T> | Use as queue |
| Stable indices | SlotMap<K, V> | Elements can be removed |
For Concurrency
| Need | Structure |
|---|
| Read-heavy | RwLock<T> |
| Write-heavy | Mutex<T> |
| Lock-free reads | DashMap<K, V> |
| Atomic counters | AtomicUsize |
LSP-Specific Performance
Incremental Indexing
Don't re-index everything on change:
fn on_file_changed(&mut self, uri: &Url) {
self.index.remove_file(uri);
if let Ok(content) = self.read_file(uri) {
self.index_file(uri, &content);
}
}
fn on_file_changed(&mut self, _uri: &Url) {
self.reindex_workspace();
}
Debounce Rapid Changes
use tokio::time::{sleep, Duration};
async fn handle_did_change(&self, uri: Url) {
self.cancel_pending_reindex(&uri);
sleep(Duration::from_millis(150)).await;
self.reindex_document(&uri).await;
}
Limit Completion Results
const MAX_COMPLETIONS: usize = 100;
fn get_completions(&self) -> Vec<CompletionItem> {
self.all_symbols()
.filter(|s| s.matches_prefix(prefix))
.take(MAX_COMPLETIONS)
.map(|s| s.to_completion_item())
.collect()
}
Use Prefix Trees for Completion
use radix_trie::Trie;
struct CompletionIndex {
constants: Trie<String, ConstantInfo>,
methods: Trie<String, MethodInfo>,
}
impl CompletionIndex {
fn complete_constant(&self, prefix: &str) -> impl Iterator<Item = &ConstantInfo> {
self.constants
.get_raw_descendant(prefix)
.into_iter()
.flat_map(|subtrie| subtrie.values())
}
}
Performance Budgets
Target Latencies
| Operation | Target | Max |
|---|
| Completion | 50ms | 100ms |
| Go-to-definition | 20ms | 50ms |
| Hover | 20ms | 50ms |
| Find references | 100ms | 500ms |
| Initial indexing (10K files) | 5s | 10s |
| Incremental reindex (1 file) | 50ms | 200ms |
Memory Targets
| Metric | Target |
|---|
| Base memory | < 50MB |
| Per 1K files indexed | + 20MB |
| Peak during indexing | < 2x steady state |
Performance Logging
Add timing to critical paths:
use std::time::Instant;
use log::info;
pub async fn handle_completion(params: CompletionParams) -> Vec<CompletionItem> {
let start = Instant::now();
let result = compute_completions(params).await;
let elapsed = start.elapsed();
if elapsed > Duration::from_millis(50) {
warn!("[PERF] Slow completion: {:?}", elapsed);
} else {
info!("[PERF] Completion in {:?}", elapsed);
}
result
}
Optimization Checklist
Before Optimizing
Common Wins
After Optimizing