top_drives/docs/ARCHITECTURE.md
Jan-Peter von Hunnius a9a46ee551 refactor: modular architecture with unified error handling and DashMap concurrency
- Break monolithic records.rs into commands/, output/, parsing/ modules
- Add lib.rs with shared types (AppError, Car, BestCarRecord, etc.)
- Replace Arc<Mutex<HashMap>> with DashMap for concurrent access
- Add unified AppError enum with thiserror
- Add configurable CLI paths (--my-cars, --cars-db, --times-dir, --tracks-db)
- Add serde deserialization for Car struct
- Add CSV escaping for fields with commas/quotes/newlines
- Fix track name splitting: split_once -> rsplit_once for _a handling
- Add integration tests, proptest property-based tests, and unit tests
- Add rustfmt.toml configuration
- Update documentation (API, ARCHITECTURE, DATA_STRUCTURES, README)
2026-05-08 16:19:01 +02:00

9.3 KiB

Architecture Documentation

System Overview

The Top Drives Records Analyzer consists of a main analysis binary and a shared library working together to provide comprehensive car performance analysis for the Top Drives racing game.

Component Architecture

1. Shared Library (src/lib.rs)

Common types and utilities shared across the project.

Key Responsibilities

  • Data Types: Defines Car, BestCarRecord, AnalysisContext, CarCounts, MyCarsData, AppError
  • Track Name Translation: Converts track codes to human-readable names via translate_track_name()
  • Track Type Translation: Converts condition codes to surface descriptions via translate_track_type()
  • File Download: Async file download utility via get_file()
  • User Data Loading: Parses my_cars.json via load_my_cars_data()
  • Car Comparison: Sorting comparator via compare_cars()
  • Count Aggregation: Per-car win count computation via compute_car_counts()
  • CSV Escaping: Safe CSV field formatting via escape_csv_field()

Error Handling

All public functions return Result<T, AppError> where AppError is a unified error type:

  • AppError::Io — File system errors
  • AppError::Network — HTTP/download errors
  • AppError::Parse — JSON parsing errors with file context
  • AppError::MissingFile — Required data file not found
  • AppError::Poisoned — Mutex poisoning from panicked threads

2. Records Binary (src/records.rs)

Primary analysis engine that processes pre-downloaded JSON timing data.

Key Responsibilities

  • Data Fetching: Downloads car and track databases from GitHub repository
  • File Processing: Parses JSON timing files for each car configuration
  • Performance Analysis: Compares timing data across RQ ranges and track conditions
  • Collection Management: Identifies owned cars and optimization opportunities
  • CSV Output: Writes analysis results to CSV files with proper escaping

Key Constants

  • MAX_RQ: u8 = 113 — Maximum RQ level used for range analysis
  • MIN_VALID_TIME: f64 = 1.2 — Minimum valid track time threshold
  • TIME_TOLERANCE: f64 = 0.005 — Tolerance for tie detection (5ms)
  • TEST_BOWL_FACTOR: f64 = 2000.0 — Multiplier for Test Bowl time inversion
  • CONCURRENCY_LIMIT: usize = 20 — Maximum concurrent tasks

Data Flow

GitHub Repository
       ↓
cars_repo.json + tracks_repo.json
       ↓
times/*.json (1000+ car files)
       ↓
JSON Parsing → BestCarRecord Analysis
       ↓
User Collection (my_cars.json)
       ↓
Output: CSV files / Terminal Results

Data Structures

Core Types

TripletKey

type TripletKey = (String, u8, String);
//   ↓           ↓        ↓
// Track ID    RQ Level  Surface Condition

Purpose: Composite key for uniquely identifying track conditions Usage: HashMap key for storing best car records

Car

#[derive(Debug, Clone, Deserialize)]
struct Car {
    rid: String,        // Repository ID
    brand: String,      // Manufacturer
    only_name: String,  // Model name
    year: u16,          // Model year
    rq: u8,             // Rarity level
    prize: bool,        // Prize car status
    guid: String,       // Game GUID
}

BestCarRecord

struct BestCarRecord {
    car_rid: String,   // Car identifier
    brand: String,     // Manufacturer
    only_name: String, // Model name
    year: u16,         // Year
    rq: u8,            // RQ level
    config: String,    // Tune config (111, 233, etc.)
    time: f64,         // Track time (seconds)
    owned: bool,       // User ownership status
    prize: bool,       // Prize car status
    guid: String,      // Game identifier
}

State Management

AnalysisContext

struct AnalysisContext {
    best_cars: Arc<Mutex<HashMap<TripletKey, Vec<BestCarRecord>>>>,
}

Thread Safety: Protected by Mutex for concurrent access during parsing Lifetime: Created in main(), passed through the call chain Access: lock_best_cars() method returns Result with proper error handling

CarCounts

struct CarCounts {
    by_rid: HashMap<String, (BestCarRecord, usize)>,
    guid_to_rid: HashMap<String, String>,
    car_map: HashMap<String, Car>,
    car_list: Vec<Car>,
}

Purpose: Aggregated per-car win counts computed once from AnalysisContext Usage: Shared across all output functions to avoid redundant computation

Algorithm Details

RQ Range Analysis

The system analyzes car performance across multiple RQ brackets:

  1. Base RQ: Car's natural rarity level
  2. RQ Range: Tests from car's RQ up to MAX_RQ (113)
  3. Cross-RQ Competition: Allows cars to compete above their base level

Example: A RQ 85 car might perform well at RQ 90-95 levels against higher-rated cars

Time Validation

Multiple validation layers ensure data quality:

  1. Minimum Time: Times below MIN_VALID_TIME (1.2s) are discarded
  2. Downvote Filter: Community-downvoted times are excluded
  3. Tie Detection: Times within TIME_TOLERANCE (5ms) are considered equal
  4. Special Cases: Test Bowl track uses inverted time calculation (TEST_BOWL_FACTOR / time)

Concurrent Processing

Bounded Concurrency Pattern

  • Task Spawning: One blocking task per car file via tokio::task::spawn_blocking
  • Semaphore: Limits concurrent operations to CONCURRENCY_LIMIT (20)
  • Error Isolation: Individual car failures don't stop batch processing

Synchronization

  • Join Handles: Waits for all parsing tasks to complete
  • Mutex Protection: Safe concurrent access to shared AnalysisContext

File Organization

Input Files

├── cars_repo.json          # Car database from repository
├── tracks_repo.json        # Track definitions
├── track_names.json        # Track code → name mappings (compiled in)
├── my_cars.json            # User's car collection (game export)
└── times/                  # Individual car timing files
    ├── Ferrari_F40_1987.json
    ├── McLaren_F1_1993.json
    └── ...

Output Files

├── best.csv              # All cars best on at least one track
├── very_best.csv         # Cars best on ≥ min_count tracks
├── details.csv           # Detailed timing data for all combinations
├── trash.csv             # Owned cars safe to remove
├── wishlist.csv          # Ideal wishlist (very_best unowned, capped)
├── add.csv               # Cars to add to wishlist
└── remove.csv            # Cars to remove from wishlist

Performance Characteristics

Records Module

  • Startup Time: Fast (uses cached JSON files)
  • Memory Usage: Moderate (loads all timing data into memory)
  • Accuracy: High (comprehensive dataset)
  • Update Frequency: Manual refresh required
  • Concurrency: Bounded at 20 simultaneous operations via semaphore

Error Handling Strategy

Unified Error Type

All errors are represented as AppError:

  • I/O Errors: File read/write failures
  • Network Errors: Download failures with descriptive messages
  • Parse Errors: JSON parsing failures with file name context
  • Missing Files: Specific variant for absent data files
  • Poisoned Mutex: Thread panic detection

Graceful Degradation

  • Missing User Data: load_my_cars_or_warn() returns empty data with a warning
  • Network Errors: Fetch errors logged but don't crash
  • Parse Failures: Individual car file failures emit warnings and skip

User Feedback

  • Progress Indication: Shows fetching/download progress
  • Error Messages: Clear descriptions via AppError Display impl
  • Warning Messages: Non-fatal issues reported via stderr

Extension Points

Adding New Tracks

  1. Update tracks_repo.json with new track definitions
  2. Add track name mapping in track_names.json
  3. Add condition mappings in translate_track_type() if needed

Adding New Analysis Types

  1. Create new output function(s) using write_sorted_csv
  2. Add command variant to the Commands enum
  3. Add command handler in main()
  4. Update help text and documentation

Adding New Output Formats

  1. Create new output function(s) alongside existing CSV writers
  2. Reuse CarCounts for data access
  3. Add CLI flag for format selection if needed

Security Considerations

Network Access

  • HTTPS Only: All external requests use HTTPS
  • Rate Limiting: Respects server limits with controlled concurrency

Local File Access

  • Read Only: No modification of game files
  • User Data: Only reads exported collection data
  • No Persistence: Analysis results are output-only

Future Enhancements

Potential Improvements

  1. Caching Layer: Cache parsed results between runs
  2. Database Backend: Replace in-memory HashMap with persistent storage
  3. Configuration Files: External configuration for analysis parameters
  4. GUI Interface: Web-based interface for non-technical users
  5. Export Formats: Multiple output formats (JSON, XML, etc.)
  6. Module Split: Break records.rs into a module tree for better organization
  7. Concurrent HashMap: Use DashMap for finer-grained locking

Scalability Considerations

  1. Pagination: Handle large datasets with pagination
  2. Incremental Updates: Only fetch changed data
  3. Compression: Compress large timing files
  4. Streaming: Process data without loading everything into memory