Benchmarks - Performance

Benchmarks de performance avec Criterion pour mesurer et optimiser les opérations critiques.

Objectifs Performance :

  • Latency P99 : < 5ms

  • Throughput : > 100,000 req/s

  • Memory : < 128MB par instance

Structure

backend/benches/
├── building_benchmarks.rs
├── unit_benchmarks.rs
├── expense_benchmarks.rs
└── query_benchmarks.rs

Criterion

Installation :

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

[[bench]]
name = "building_benchmarks"
harness = false

Exemple Benchmark :

// benches/building_benchmarks.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use koprogo_api::domain::entities::Building;

fn benchmark_building_creation(c: &mut Criterion) {
    c.bench_function("building creation", |b| {
        b.iter(|| {
            Building::new(
                black_box("Benchmark Building".to_string()),
                black_box("123 Benchmark St".to_string()),
                black_box("Paris".to_string()),
                black_box("75001".to_string()),
                black_box("France".to_string()),
                black_box(50),
                black_box(Some(2020)),
            )
        });
    });
}

fn benchmark_building_update(c: &mut Criterion) {
    let mut building = Building::new(
        "Original Name".to_string(),
        "Original Address".to_string(),
        "Paris".to_string(),
        "75001".to_string(),
        "France".to_string(),
        30,
        None,
    ).unwrap();

    c.bench_function("building update", |b| {
        b.iter(|| {
            building.update_info(
                black_box("New Name".to_string()),
                black_box("New Address".to_string()),
                black_box("Lyon".to_string()),
                black_box("69001".to_string()),
            )
        });
    });
}

criterion_group!(
    benches,
    benchmark_building_creation,
    benchmark_building_update
);
criterion_main!(benches);

Exécution :

# Tous les benchmarks
cargo bench

# Benchmark spécifique
cargo bench building_creation

# Output: target/criterion/report/index.html

Benchmarks Database

Query Performance :

// benches/query_benchmarks.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
use sqlx::PgPool;
use tokio::runtime::Runtime;

async fn setup_db() -> PgPool {
    let pool = PgPool::connect("postgresql://...")
        .await
        .unwrap();

    // Seed test data
    seed_test_data(&pool).await;

    pool
}

fn benchmark_find_by_id(c: &mut Criterion) {
    let rt = Runtime::new().unwrap();
    let pool = rt.block_on(setup_db());
    let repo = PostgresBuildingRepository::new(pool);

    let test_id = Uuid::new_v4();

    c.bench_function("find building by id", |b| {
        b.to_async(&rt).iter(|| async {
            repo.find_by_id(black_box(test_id)).await
        });
    });
}

fn benchmark_paginated_query(c: &mut Criterion) {
    let rt = Runtime::new().unwrap();
    let pool = rt.block_on(setup_db());
    let repo = PostgresBuildingRepository::new(pool);

    let org_id = Uuid::new_v4();

    c.bench_with_input(
        BenchmarkId::new("paginated query", "page_size"),
        &20i64,
        |b, &per_page| {
            b.to_async(&rt).iter(|| async {
                repo.find_all_paginated(
                    black_box(org_id),
                    black_box(1),
                    black_box(per_page)
                ).await
            });
        }
    );
}

criterion_group!(
    benches,
    benchmark_find_by_id,
    benchmark_paginated_query
);
criterion_main!(benches);

Benchmarks Expense Calculator

// benches/expense_benchmarks.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use koprogo_api::domain::services::expense_calculator::ExpenseCalculator;

fn benchmark_calculate_share(c: &mut Criterion) {
    let expense = Expense {
        id: Uuid::new_v4(),
        building_id: Uuid::new_v4(),
        description: "Test Expense".to_string(),
        amount: 100000,  // 1000.00€
        // ...
    };

    let unit = Unit {
        id: Uuid::new_v4(),
        building_id: expense.building_id,
        unit_number: "A-1".to_string(),
        floor: 1,
        surface_area: 75,
        ownership_share: 45,  // 45/1000
        // ...
    };

    c.bench_function("calculate expense share", |b| {
        b.iter(|| {
            ExpenseCalculator::calculate_share(
                black_box(&expense),
                black_box(&unit),
                black_box(1000),  // Total shares
            )
        });
    });
}

fn benchmark_calculate_all_shares(c: &mut Criterion) {
    let expense = create_test_expense();
    let units: Vec<Unit> = (0..50)
        .map(|i| create_test_unit(i))
        .collect();

    c.bench_function("calculate all shares (50 units)", |b| {
        b.iter(|| {
            for unit in black_box(&units) {
                ExpenseCalculator::calculate_share(&expense, unit, 1000);
            }
        });
    });
}

criterion_group!(
    benches,
    benchmark_calculate_share,
    benchmark_calculate_all_shares
);
criterion_main!(benches);

Benchmarks API Handlers

// benches/api_benchmarks.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use actix_web::{test, web, App};
use tokio::runtime::Runtime;

fn benchmark_list_buildings_endpoint(c: &mut Criterion) {
    let rt = Runtime::new().unwrap();

    rt.block_on(async {
        let app = test::init_service(
            App::new()
                .app_data(web::Data::new(test_app_state()))
                .configure(configure_routes)
        ).await;

        c.bench_function("GET /buildings (20 results)", |b| {
            b.to_async(&rt).iter(|| async {
                let req = test::TestRequest::get()
                    .uri("/api/v1/buildings?page=1&per_page=20")
                    .insert_header(("Authorization", "Bearer test-token"))
                    .to_request();

                let resp = test::call_service(&app, req).await;
                black_box(resp);
            });
        });
    });
}

fn benchmark_create_building_endpoint(c: &mut Criterion) {
    let rt = Runtime::new().unwrap();

    rt.block_on(async {
        let app = test::init_service(
            App::new()
                .app_data(web::Data::new(test_app_state()))
                .configure(configure_routes)
        ).await;

        c.bench_function("POST /buildings", |b| {
            b.to_async(&rt).iter(|| async {
                let req = test::TestRequest::post()
                    .uri("/api/v1/buildings")
                    .insert_header(("Authorization", "Bearer test-token"))
                    .set_json(&create_test_building_dto())
                    .to_request();

                let resp = test::call_service(&app, req).await;
                black_box(resp);
            });
        });
    });
}

criterion_group!(
    benches,
    benchmark_list_buildings_endpoint,
    benchmark_create_building_endpoint
);
criterion_main!(benches);

Résultats Typiques

Domain Operations :

building creation       time:   [125.32 ns 126.45 ns 127.89 ns]
building update         time:   [78.91 ns 79.34 ns 79.82 ns]
calculate expense share time:   [42.15 ns 42.67 ns 43.24 ns]

Database Queries :

find building by id     time:   [1.234 ms 1.278 ms 1.325 ms]
paginated query (20)    time:   [2.456 ms 2.512 ms 2.578 ms]
create building (DB)    time:   [1.567 ms 1.612 ms 1.665 ms]

API Endpoints :

GET /buildings (20)     time:   [3.234 ms 3.312 ms 3.398 ms]
POST /buildings         time:   [2.123 ms 2.189 ms 2.267 ms]
PUT /buildings/:id      time:   [2.345 ms 2.412 ms 2.489 ms]

Optimisations

Compilation Release :

[profile.release]
opt-level = 3           # Optimisation maximale
lto = true              # Link-Time Optimization
codegen-units = 1       # Codegen units minimal
strip = true            # Strip symbols
panic = "abort"         # Panic = abort (plus rapide)

Connection Pool :

PgPoolOptions::new()
    .max_connections(10)         # Max 10 connexions
    .min_connections(2)          # Min 2 connexions
    .acquire_timeout(Duration::from_secs(30))
    .idle_timeout(Duration::from_secs(300))
    .connect(&database_url)
    .await

Indexes Database :

CREATE INDEX idx_buildings_org ON buildings(organization_id);
CREATE INDEX idx_buildings_city ON buildings(city);
CREATE INDEX idx_units_building ON units(building_id);
CREATE INDEX idx_expenses_building ON expenses(building_id);
CREATE INDEX idx_expenses_status ON expenses(payment_status);

Profiling

Flamegraph :

# Installer
cargo install flamegraph

# Générer flamegraph
cargo flamegraph --bench building_benchmarks

# Output: flamegraph.svg

Perf (Linux) :

# Profiler avec perf
perf record --call-graph=dwarf cargo bench

# Analyser
perf report

Valgrind (Memory profiling) :

# Installer
sudo apt-get install valgrind

# Profiler mémoire
valgrind --tool=massif cargo bench

# Visualiser
ms_print massif.out.*

Comparaison Versions

Baseline : Sauvegarder résultats pour comparaison.

# Sauvegarder baseline
cargo bench -- --save-baseline main

# Après modifications
cargo bench -- --baseline main

# Criterion affichera les différences

Métriques Cibles

Opération

Cible

Actuel

Building creation (domain)

< 200ns

~125ns ✅

Building query (DB)

< 2ms

~1.3ms ✅

Paginated query (20)

< 5ms

~2.5ms ✅

API GET /buildings

< 5ms P99

~3.3ms ✅

API POST /buildings

< 5ms P99

~2.2ms ✅

CI/CD Benchmarks

Régression Detection :

# .github/workflows/bench.yml
name: Benchmarks

on:
  pull_request:
    branches: [main]

jobs:
  benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install Rust
        uses: actions-rs/toolchain@v1

      - name: Run benchmarks
        run: cargo bench

      - name: Compare with main
        run: cargo bench -- --baseline main

      - name: Alert on regression
        if: regression_detected
        run: echo "Performance regression detected!"

Références