# Performance Analysis Report

**Date:** 2026-02-11
**Analyzed by:** Claude
**Status:** ✅ FIXED

## Executive Summary

A **critical performance bottleneck** was identified and fixed: regex compilation was happening on every meta-line parse, causing ~800MB of allocations for a 1MB input file.

### Results

| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| 1MB processing time | 3.80s | 0.022s | **172x faster** |
| Lines/second (1MB) | 5,700 | 990,000 | **172x** |
| Lines/second (100MB) | N/A | 2,300,000 | - |
| Total allocations (1MB) | 824 MB | 15 MB | **55x less** |

### Fix Applied

**File:** `src/parsers/wasot/parser.rs:188-200`

Changed from compiling regex on every call to using `OnceLock` for single compilation:

```rust
// BEFORE (bad - compiled every call)
let regex = Regex::new(pattern).unwrap();

// AFTER (good - compiled once)
static ID_REGEX: OnceLock<Regex> = OnceLock::new();
let regex = ID_REGEX.get_or_init(|| {
    Regex::new(r"\s:[A-Za-z0-9\-_:\p{L}]+\s*$").unwrap()
});
```

## Methodology

- DHAT heap profiling with `--features dhat-heap`
- Allocation analysis of `dhat-heap.json`
- Call stack analysis to identify hotspots

## Key Findings

### 1. CRITICAL: Regex Compiled Per Line

**Location:** `src/parsers/wasot/parser.rs:200`

```rust
fn extract_id_string(input: &str) -> (String, String) {
    let pattern = r"\s:[A-Za-z0-9\-_:\p{L}]+\s*$";
    let regex = Regex::new(pattern).unwrap();  // <-- COMPILED EVERY CALL!
    ...
}
```

**Impact:**
- 810,369,484 bytes (810 MB) allocated for regex operations
- Called once per meta-line (~22,234 times for 1MB file)
- Regex compilation is expensive (~300KB allocations per compile)

**Fix:**
```rust
use std::sync::OnceLock;
use regex::Regex;

fn get_id_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r"\s:[A-Za-z0-9\-_:\p{L}]+\s*$").unwrap()
    })
}

fn extract_id_string(input: &str) -> (String, String) {
    let regex = get_id_regex();
    // ... rest of function
}
```

### 2. Allocation Statistics

| Metric | Value |
|--------|-------|
| Total bytes allocated | 824,842,464 (824 MB) |
| Total allocation blocks | 2,796,953 |
| Peak heap usage | 662,383 bytes (646 KB) |
| Final heap usage | 1,044 bytes |

**Top 5 Allocation Sites:**

| Rank | Bytes | Source |
|------|-------|--------|
| 1 | 316 MB | `regex_automata::nfa::thompson::map::Utf8BoundedMap::clear` |
| 2 | 64 MB | `regex_automata::nfa::thompson::builder::Builder::add` |
| 3 | 64 MB | `regex_automata::nfa::thompson::builder::Builder::add` |
| 4 | 48 MB | `regex_automata::nfa::thompson::nfa::Inner::add` |
| 5 | 48 MB | `regex_automata::nfa::thompson::nfa::Inner::add` |

All top 5 are regex NFA construction - this confirms the regex compilation is the primary issue.

### 3. Call Stack Analysis

The allocation call path:

```
main.rs:1053 (process_info)
  → wasot_file_emitter.rs:313 (Emitter::run)
    → wasot_file_emitter.rs:177 (process_line_with_events)
      → parser.rs:278 (MetaLine::parse)
        → parser.rs:200 (extract_id_string)
          → Regex::new() ← HOTSPOT
```

## Performance Comparison

### Current (with bug)
```
File: 1mb.wasot (1 MB, 22,234 lines)
Time: 3.80s
Rate: ~5,700 lines/second
Allocations: 824 MB
```

### Expected (after fix)
```
File: 1mb.wasot (1 MB, 22,234 lines)
Time: ~0.2s (estimated)
Rate: >100,000 lines/second
Allocations: ~10-20 MB
```

## Recommended Fixes

### Priority 1: Fix Regex Compilation (CRITICAL)

**File:** `src/parsers/wasot/parser.rs`

Replace lines 199-200:
```rust
// BEFORE (bad)
let pattern = r"\s:[A-Za-z0-9\-_:\p{L}]+\s*$";
let regex = Regex::new(pattern).unwrap();

// AFTER (good)
static ID_REGEX: OnceLock<Regex> = OnceLock::new();
let regex = ID_REGEX.get_or_init(|| {
    Regex::new(r"\s:[A-Za-z0-9\-_:\p{L}]+\s*$").unwrap()
});
```

**Expected impact:** 15-20x speedup, 40x reduction in allocations

### Priority 2: Consider Regex-Free Parsing

For even better performance, replace the regex with manual parsing:

```rust
fn extract_id_string(input: &str) -> (String, String) {
    // Find last occurrence of whitespace followed by colon
    if let Some(pos) = input.rfind(|c: char| c.is_whitespace()) {
        let rest = &input[pos..].trim_start();
        if rest.starts_with(':') {
            // Validate ID characters manually
            // ...
        }
    }
    (String::new(), input.to_string())
}
```

**Expected impact:** Additional 2-3x speedup over cached regex

### Priority 3: Reduce String Allocations

The function creates new Strings unnecessarily:

```rust
// Current
let cleaned_string = input.replace(match_str, "").trim_end().to_string();

// Better - return slices where possible
fn extract_id_string(input: &str) -> (Option<&str>, &str)
```

## Verification

After implementing fixes:

1. Run benchmark:
   ```bash
   ./scripts/benchmark_cli.py --quick --force
   ```

2. Compare results with this report's baseline

3. Run DHAT again to verify allocation reduction:
   ```bash
   cargo build --release --features dhat-heap
   ./target/release/wasot data/bench/1mb.wasot -t wasot > /dev/null
   # Check dhat-heap.json - total bytes should be < 50MB
   ```

## Appendix: DHAT Frame Analysis

Top frames by allocation volume:

```
regex_automata::nfa::thompson::map::Utf8BoundedMap::clear     316 MB
regex_automata::nfa::thompson::builder::Builder::add          129 MB
regex_automata::nfa::thompson::nfa::Inner::add                121 MB
regex_syntax::utf8::Utf8Sequences::new                         11 MB
regex_automata::nfa::thompson::compiler::Utf8Node              22 MB
```

All top allocators are regex-related, confirming the diagnosis.

## Approach & Tools Used

### 1. Benchmarking Script

Created `scripts/benchmark_cli.py` to measure performance:

```bash
# Quick single-run benchmark
./scripts/benchmark_cli.py --quick

# Full benchmark with multiple runs
./scripts/benchmark_cli.py

# Specific sizes and formats
./scripts/benchmark_cli.py --sizes 1kb 1mb --formats wasot,xml
```

Features:
- Generates test files automatically
- Measures lines/second throughput
- Stores results in JSON with git commit and binary MD5
- Skips re-running if same binary already benchmarked

### 2. Profiling Script

Created `scripts/profile.py` for DHAT heap analysis:

```bash
# Run DHAT profile
./scripts/profile.py

# Analyze existing profile
./scripts/profile.py --analyze

# Compare two profiles
./scripts/profile.py --compare before.json after.json
```

Features:
- Builds with DHAT support automatically
- Analyzes allocation hotspots
- Identifies common issues (regex in loops, high allocations)
- Compares before/after profiles

### 3. Iterative Approach

1. **Baseline measurement**: `./scripts/benchmark_cli.py --quick`
2. **Profile**: `./scripts/profile.py`
3. **Identify hotspot**: Look for high `tb` (total bytes) in DHAT output
4. **Fix**: Apply optimization
5. **Verify**: Re-run benchmark and profile
6. **Document**: Update reports with findings

### 4. Key Learnings

- **Regex compilation is expensive**: ~300KB allocations per `Regex::new()`
- **Use `OnceLock` for static patterns**: Compiles once, zero runtime cost after
- **DHAT is excellent for finding allocation hotspots**: Shows exact call stacks
- **Lines/second is a better metric than MB/s**: More meaningful for text processing

## Final Performance

After fix, wasot achieves:

| File Size | Lines | Time | Rate |
|-----------|-------|------|------|
| 1 KB | 32 | 4ms | 7.6K lines/s |
| 10 KB | 239 | 4ms | 57K lines/s |
| 500 KB | 10,898 | 14ms | 794K lines/s |
| 1 MB | 22,234 | 22ms | 990K lines/s |
| 100 MB | 2,213,868 | 955ms | 2.3M lines/s |

The 100MB file processes at **2.3 million lines per second**.
