Skip to content

Performance: Eliminate string allocations in request metrics hot path #638

@jeremyandrews

Description

@jeremyandrews

GitHub Issue: Performance: Eliminate string allocations in request metrics hot path

Problem

Every HTTP request creates 4-6 unnecessary string allocations in the GooseRequestMetric::new() constructor, causing significant memory pressure and CPU overhead in the hot path. This happens in src/goose.rs:

// In GooseRequestMetric::new() - these allocate on EVERY request
pub struct GooseRequestMetric {
    pub scenario_name: String,           // Heap allocation #1
    pub transaction_index: String,       // Heap allocation #2  
    pub transaction_name: String,        // Heap allocation #3
    pub name: String,                    // Heap allocation #4
    // ... other fields
}

// In the constructor:
GooseRequestMetric {
    scenario_name: transaction_detail.scenario_name.to_string(),     // Clone
    transaction_index: transaction_detail.transaction_index.to_string(), // Clone
    transaction_name: transaction_detail.transaction_name.to_string(),   // Clone
    name: name.to_string(),                                              // Clone
    // ...
}

Performance Impact

  • 4-6 heap allocations per HTTP request (most critical bottleneck)
  • String cloning overhead for data that's already available elsewhere
  • Memory fragmentation from millions of small string allocations
  • Scale: In a 1M request test, this creates 4-6M unnecessary allocations
  • GC pressure: Significantly increases allocation rate

Proposed Solution: Reference Existing Data

Instead of copying strings, reference the data that already exists in GooseAttack using simple indices:

pub struct GooseRequestMetric {
    // Replace String with lightweight references to existing data
    pub scenario_index: usize,              // Index into scenarios Vec
    pub transaction_index: usize,           // Index into transactions Vec  
    pub name: String,                       // Keep this one since it varies per request
    // ... other fields remain unchanged
}

// Access existing strings without allocation:
impl GooseRequestMetric {
    pub fn get_scenario_name(&self, goose_attack: &GooseAttack) -> &str {
        &goose_attack.scenarios[self.scenario_index].name
    }
    
    pub fn get_transaction_name(&self, goose_attack: &GooseAttack) -> &str {
        &goose_attack.scenarios[self.scenario_index]
            .transactions[self.transaction_index].name
    }
}

// Constructor becomes allocation-free for most fields:
impl GooseRequestMetric {
    pub fn new(
        scenario_index: usize,
        transaction_index: usize,
        name: String,                       // Only allocate for the request-specific name
        // ... other params
    ) -> Self {
        GooseRequestMetric {
            scenario_index,
            transaction_index,
            name,                           // Only 1 allocation instead of 4-6
            // ... other fields
        }
    }
}

Why This Approach

  • Zero external dependencies - uses existing GooseAttack data structures
  • Minimal code changes - just change field types and constructor
  • Same performance benefits - eliminates 4-6 unnecessary allocations per request
  • No complexity overhead - straightforward index approach
  • Leverages existing architecture - GooseAttack already stores all scenario/transaction names

Expected Performance Gains

  • Memory reduction: 60-80% fewer allocations in request hot path (4-5 allocations eliminated per request)
  • CPU improvement: Faster metrics creation without string cloning
  • Simplicity: Minimal code changes with maximum impact
  • GC pressure reduction: Significantly less memory allocation/deallocation churn
  • Maintainability: Uses existing GooseAttack architecture without adding complexity

Implementation Plan

  1. Phase 1: Change String fields to indices that reference existing collections
  2. Phase 2: Update GooseRequestMetric::new() to use indices instead of cloning
  3. Phase 3: Update display/serialization code to resolve indices to strings

Testing Strategy

  • Unit tests: Verify index-based lookups work correctly
  • Integration tests: Ensure metrics display and serialization work
  • Performance tests: Benchmark allocation reduction
  • Memory profiling: Confirm heap allocation improvement
  • Load tests: Validate under realistic high-load scenarios

Acceptance Criteria

  • GooseRequestMetric uses indices instead of owned strings for scenario/transaction names
  • Eliminates 4-5 unnecessary string allocations in GooseRequestMetric::new()
  • All display functionality works correctly
  • Serialization/deserialization preserved
  • Performance benchmarks show 60-80% allocation reduction
  • Memory usage reduced significantly
  • All tests pass

Related Issues

Part of comprehensive performance optimization effort. Related to:

Implementation Notes

  • Scenario and transaction names already exist in GooseAttack structure
  • Index bounds should be validated to prevent panics
  • Display code needs access to GooseAttack for index->string resolution
  • Only the request name field remains as String since it varies per request

Labels

  • enhancement
  • breaking-change

Priority

Priority 1 (Critical Impact)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions