diff --git a/CODEBASE_OVERVIEW.md b/CODEBASE_OVERVIEW.md new file mode 100644 index 00000000..9c84ba89 --- /dev/null +++ b/CODEBASE_OVERVIEW.md @@ -0,0 +1,866 @@ +# Flow Library - Codebase Overview + +> **C++14 Header-Only Library for Multi-Stream Data Synchronization** + +This document serves as the primary onboarding guide for developers new to the Flow codebase. It provides a comprehensive overview of the architecture, core concepts, and practical usage patterns. + +--- + +## Table of Contents + +1. [Project Overview](#1-project-overview) + - [Purpose and Functionality](#purpose-and-functionality) + - [Key Problems Solved](#key-problems-solved) + - [Real-World Use Case](#real-world-use-case) +2. [Technology Stack](#2-technology-stack) + - [Languages and Standards](#languages-and-standards) + - [Build Systems](#build-systems) + - [Dependencies](#dependencies) +3. [Project Structure](#3-project-structure) + - [Directory Layout](#directory-layout) + - [Key Directories Explained](#key-directories-explained) +4. [Architecture](#4-architecture) + - [Architectural Patterns](#architectural-patterns) + - [Core Components](#core-components) + - [Component Interaction Diagram](#component-interaction-diagram) + - [Synchronization Flow Diagram](#synchronization-flow-diagram) +5. [Core Concepts](#5-core-concepts) + - [Captors (Drivers and Followers)](#captors-drivers-and-followers) + - [Dispatch Concept](#dispatch-concept) + - [Threading Models](#threading-models) + - [Synchronization States](#synchronization-states) +6. [Code Examples](#6-code-examples) + - [Basic Synchronization](#example-1-basic-synchronization) + - [Multi-Threaded with Blocking](#example-2-multi-threaded-with-blocking) + - [Sliding Window (Batch)](#example-3-sliding-window-batch) + - [Non-Overlapping Batches (Chunk)](#example-4-non-overlapping-batches-chunk) + - [Latched State](#example-5-latched-state) + - [Exact Timestamp Match](#example-6-exact-timestamp-match) + - [Dry-Run with Locate](#example-7-dry-run-with-locate) +7. [Execution Trace: Complete Synchronization](#7-execution-trace-complete-synchronization) +8. [Configuration and Customization](#8-configuration-and-customization) + - [Compile-Time Configuration](#compile-time-configuration) + - [Custom Dispatch Types](#custom-dispatch-types) + - [Queue Monitors](#queue-monitors) +9. [Error Handling](#9-error-handling) +10. [Building and Testing](#10-building-and-testing) +11. [Contributing](#11-contributing) + +--- + +## 1. Project Overview + +### Purpose and Functionality + +**Flow** is a C++14 header-only library designed for synchronizing multiple streams of timestamped data. Originally developed by Fetch Robotics Inc. (now under ZebraDevs), it enables data-driven event execution using data collected from distinct streaming series. + +### Key Problems Solved + +| Problem | Flow's Solution | +|---------|-----------------| +| **Cross-stream correlation** | Determines which data elements from separate streams relate to each other using timestamp-based policies | +| **Readiness detection** | Knows when synchronized data is ready for capture using Driver-Follower pattern | +| **Uniform data capture** | Captures different data types with minimal overhead using generic Dispatch concept | + +### Real-World Use Case + +In robotics (ROS), multiple sensor streams (cameras, LiDAR, IMU) need synchronization: + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Camera Feed │ │ LiDAR Scan │ │ IMU Data │ +│ (30 Hz) │ │ (10 Hz) │ │ (100 Hz) │ +└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ + └───────────────────┼───────────────────┘ + │ + ┌──────▼──────┐ + │ Flow │ + │ Synchronizer│ + └──────┬──────┘ + │ + ┌──────▼──────┐ + │ Synchronized│ + │ Data Frame │ + └─────────────┘ +``` + +--- + +## 2. Technology Stack + +### Languages and Standards + +- **C++14** (strict requirement) +- Modern C++ features: CRTP, variadic templates, perfect forwarding +- Compiler flags: `-std=c++14 -Werror -Wall -Wextra` + +### Build Systems + +**CMake** (version 3.5+): +```bash +cmake -DBUILD_TESTS=ON . +make +ctest +``` + +**Bazel**: +```bash +bazel test test/... --test_output=all +``` + +### Dependencies + +| Dependency | Purpose | Required | +|------------|---------|----------| +| C++14 Standard Library | Core functionality | Yes | +| GoogleTest | Unit testing | Tests only | +| clang-format-7 | Code formatting | Development | +| pre-commit | Git hooks | Development | + +--- + +## 3. Project Structure + +### Directory Layout + +``` +flow/ +├── include/flow/ # Public API headers (THE LIBRARY) +│ ├── flow.hpp # Main entry point +│ ├── captor.hpp # Base captor interface +│ ├── synchronizer.hpp # Synchronizer orchestrator +│ ├── dispatch.hpp # Dispatch concept +│ ├── driver/ # Driver policies +│ ├── follower/ # Follower policies +│ ├── captor/ # Threading implementations +│ ├── impl/ # ⚠️ PRIVATE - Never include directly +│ └── utility/ # Template utilities +├── test/ # GoogleTest test suite +│ └── flow/ # Tests mirror include structure +├── doc/ # Diagrams for documentation +├── bazel/ # Bazel build rules +├── cmake/ # CMake configuration +└── .github/ # CI/CD workflows +``` + +### Key Directories Explained + +| Directory | Role | Include Directly? | +|-----------|------|-------------------| +| `include/flow/` | Public API | ✅ Yes | +| `include/flow/impl/` | Implementation details | ❌ **NEVER** | +| `include/flow/driver/` | Driver capture policies | ✅ Yes | +| `include/flow/follower/` | Follower capture policies | ✅ Yes | +| `include/flow/captor/` | Threading implementations | ✅ Yes | +| `test/flow/` | Unit tests | N/A | + +--- + +## 4. Architecture + +### Architectural Patterns + +Flow uses **Policy-Based Design** with **CRTP (Curiously Recurring Template Pattern)**: + +``` +ConcretePolicy (e.g., Next, Before) + │ + ▼ +Driver or Follower ← CRTP Base + │ + ▼ +Captor ← Threading Implementation + │ + ▼ +CaptorInterface> ← Common Interface +``` + +**Benefits:** +- Zero runtime overhead (no virtual functions) +- Compile-time polymorphism +- Type-safe policy composition + +### Core Components + +| Component | Responsibility | +|-----------|----------------| +| **Synchronizer** | Orchestrates capture across captor tuples | +| **Driver** | Establishes synchronization time range | +| **Follower** | Selects data relative to driver's range | +| **DispatchQueue** | Ordered buffer for timestamped data | +| **Dispatch** | Timestamp + data payload wrapper | + +### Component Interaction Diagram + +```mermaid +graph TB + subgraph "User Code" + UC[Application] + end + + subgraph "Synchronization Layer" + SYNC[Synchronizer] + end + + subgraph "Captor Instances" + D[Driver
Next, Batch, Chunk] + F1[Follower 1
Before, ClosestBefore] + F2[Follower 2
Latched, Ranged] + end + + subgraph "Data Layer" + DQ1[DispatchQueue] + DQ2[DispatchQueue] + DQ3[DispatchQueue] + end + + UC -->|"inject(stamp, data)"| D + UC -->|"inject(stamp, data)"| F1 + UC -->|"inject(stamp, data)"| F2 + UC -->|"capture(captors, outputs)"| SYNC + + SYNC -->|"1. locate()"| D + SYNC -->|"2. locate(range)"| F1 + SYNC -->|"2. locate(range)"| F2 + SYNC -->|"3. extract()"| D + SYNC -->|"3. extract()"| F1 + SYNC -->|"3. extract()"| F2 + + D --> DQ1 + F1 --> DQ2 + F2 --> DQ3 + + SYNC -->|"Result{state, range}"| UC +``` + +### Synchronization Flow Diagram + +```mermaid +sequenceDiagram + participant User + participant Sync as Synchronizer + participant Driver + participant Follower + participant Queue as DispatchQueue + + Note over User,Queue: Phase 1: Data Injection + User->>Driver: inject(stamp, data) + Driver->>Queue: insert(Dispatch) + User->>Follower: inject(stamp, data) + Follower->>Queue: insert(Dispatch) + + Note over User,Queue: Phase 2: Synchronization + User->>Sync: capture(captors, outputs) + + Sync->>Driver: locate() + Driver->>Queue: oldest_stamp() + Queue-->>Driver: stamp=10 + Driver-->>Sync: PRIMED, range={10,10} + + Sync->>Follower: locate(range={10,10}) + Follower->>Queue: find elements + Follower-->>Sync: PRIMED + + Note over User,Queue: Phase 3: Extraction + Sync->>Driver: extract() + Driver->>Queue: move & remove + Sync->>Follower: extract() + Follower->>Queue: move & remove + + Sync-->>User: Result{PRIMED, {10,10}} +``` + +--- + +## 5. Core Concepts + +### Captors (Drivers and Followers) + +**Drivers** establish the synchronization time range: + +| Driver | Behavior | Use Case | +|--------|----------|----------| +| `driver::Next` | Captures oldest element | Frame-by-frame processing | +| `driver::Batch` | Captures N elements, removes oldest | Sliding window | +| `driver::Chunk` | Captures N elements, removes all | Non-overlapping batches | +| `driver::Throttled` | Rate-limited capture | Bandwidth control | + +**Followers** select data relative to driver's range: + +| Follower | Behavior | Use Case | +|----------|----------|----------| +| `follower::Before` | All elements before boundary | Historical context | +| `follower::ClosestBefore` | Single closest element | Nearest-neighbor matching | +| `follower::Latched` | Maintains last value | Configuration state | +| `follower::MatchedStamp` | Exact timestamp match | Strictly synchronized streams | +| `follower::Ranged` | Elements spanning range | All events in window | +| `follower::CountBefore` | N elements before boundary | Fixed history size | +| `follower::AnyBefore` | Optional (allows empty) | Non-critical streams | + +### Dispatch Concept + +Data must provide timestamp and value access: + +```cpp +// Default dispatch template +template +class Dispatch { +public: + StampT stamp; // Sequencing timestamp + ValueT value; // Data payload +}; + +// For custom types, specialize traits: +namespace flow { + template<> struct DispatchTraits { + using stamp_type = MyStamp; + using value_type = MyValue; + }; + + template<> struct DispatchAccess { + static const MyStamp& stamp(const MyType& d) { return d.timestamp; } + static const MyValue& value(const MyType& d) { return d.data; } + }; +} +``` + +### Threading Models + +| Model | Lock Policy | Behavior | +|-------|-------------|----------| +| Single-threaded | `NoLock` | Zero overhead, polling | +| Multi-threaded blocking | `std::unique_lock` | Waits until data ready | +| Multi-threaded polling | `Polling` | Thread-safe, non-blocking | + +### Synchronization States + +```cpp +enum class State { + PRIMED, // ✅ Data captured successfully + RETRY, // ⏳ Need more data, try again + ABORT, // ❌ Cannot synchronize, frame dropped + TIMEOUT, // ⏱️ Multi-threaded wait expired + ERROR_DRIVER_LOWER_BOUND_EXCEEDED, // 🚨 Timestamp violation + SKIP_FRAME_QUEUE_PRECONDITION // ⏭️ Queue monitor rejected +}; +``` + +--- + +## 6. Code Examples + +### Example 1: Basic Synchronization + +```cpp +#include +#include +#include + +using namespace flow; + +// Define dispatch type +template +using MyDispatch = Dispatch; + +int main() { + // Create captors (single-threaded) + driver::Next, NoLock> driver; + follower::Before, NoLock> follower{5}; // delay=5 + + // Inject data + for (int t = 0; t < 100; ++t) { + driver.inject(t, t * 10); + follower.inject(t, "msg_" + std::to_string(t)); + } + + // Synchronization loop + while (true) { + std::vector> driver_data; + std::vector> follower_data; + + auto [result, outputs] = Synchronizer::capture( + std::forward_as_tuple(driver, follower), + std::forward_as_tuple( + std::back_inserter(driver_data), + std::back_inserter(follower_data) + ) + ); + + if (result.state == State::PRIMED) { + // Process synchronized data + std::cout << "Synced at t=" << result.range.lower_stamp << "\n"; + } else if (result.state == State::RETRY) { + break; // No more data + } + } + return 0; +} +``` + +### Example 2: Multi-Threaded with Blocking + +```cpp +#include +#include +#include +#include + +using namespace flow; +using namespace std::chrono_literals; + +using StampType = std::chrono::steady_clock::time_point; +template +using TimedDispatch = Dispatch; + +std::atomic running{true}; + +// Producer thread +void producer(driver::Next, + std::unique_lock>& driver) { + while (running) { + driver.inject(std::chrono::steady_clock::now(), + read_sensor()); + std::this_thread::sleep_for(10ms); + } +} + +// Consumer thread +void consumer(driver::Next, + std::unique_lock>& driver, + follower::ClosestBefore, + std::unique_lock>& follower) { + while (running) { + std::vector> d_data; + std::vector> f_data; + + // Blocking capture with 100ms timeout + auto [result, _] = Synchronizer::capture( + std::forward_as_tuple(driver, follower), + std::forward_as_tuple( + std::back_inserter(d_data), + std::back_inserter(f_data) + ), + StampType::min(), + std::chrono::steady_clock::now() + 100ms + ); + + if (result.state == State::PRIMED) { + process(d_data, f_data); + } + } +} +``` + +### Example 3: Sliding Window (Batch) + +```cpp +// Batch: captures N, removes only oldest → sliding window +driver::Batch driver{10}; // batch_size=10 + +for (int i = 0; i < 50; ++i) { + driver.inject(i, i * 0.5); +} + +// Capture 1: gets [0-9], removes [0] → driver has [1-49] +// Capture 2: gets [1-10], removes [1] → driver has [2-49] +// → Overlapping windows! +``` + +### Example 4: Non-Overlapping Batches (Chunk) + +```cpp +// Chunk: captures N, removes ALL captured → non-overlapping +driver::Chunk driver{5}; // chunk_size=5 + +for (int i = 0; i < 20; ++i) { + driver.inject(i, "data"); +} + +// Capture 1: gets [0-4], removes [0-4] → driver has [5-19] +// Capture 2: gets [5-9], removes [5-9] → driver has [10-19] +// → No overlap! +``` + +### Example 5: Latched State + +```cpp +// Latched: maintains last value until newer one arrives +driver::Next driver; +follower::Latched config{10}; // min_period=10 + +// Sparse config updates +config.inject(0, "v1"); +config.inject(100, "v2"); +config.inject(250, "v3"); + +// Frequent driver data +for (int t = 0; t < 300; t += 5) { + driver.inject(t, "data"); +} + +// Captures at t=50 will get "v1" (latched) +// Captures at t=150 will get "v2" (latched) +// Perfect for slow-updating configuration! +``` + +### Example 6: Exact Timestamp Match + +```cpp +// MatchedStamp: requires EXACT timestamp match +driver::Next driver; +follower::MatchedStamp follower; + +// Synchronized injection +for (int t = 0; t < 100; t += 10) { + driver.inject(t, t * 1.0); + follower.inject(t, t * 2.0); // Same timestamp! +} + +// Only captures when driver.stamp == follower.stamp +// Returns ABORT if no exact match possible +``` + +### Example 7: Dry-Run with Locate + +```cpp +// Check if capture possible WITHOUT consuming data +auto locate_result = Synchronizer::locate( + std::forward_as_tuple(driver, follower) +); + +if (locate_result.state == State::PRIMED) { + std::cout << "Would capture at [" + << locate_result.range.lower_stamp << ", " + << locate_result.range.upper_stamp << "]\n"; + + // Now actually capture (same result guaranteed) + auto capture_result = Synchronizer::capture( + std::forward_as_tuple(driver, follower), + std::forward_as_tuple(d_out, f_out) + ); +} +``` + +--- + +## 7. Execution Trace: Complete Synchronization + +A detailed trace of `Synchronizer::capture()`: + +``` +DATA STATE: + Driver queue: stamps [2, 3, 4, ..., 19] + Follower1 queue (ClosestBefore): stamps [0, 1, 2, ..., 17] + Follower2 queue (Before, delay=1): stamps [-2, -1, 0, 1, ..., 15] + +STEP 1: User calls Synchronizer::capture() +│ +├─> Create Result{state: RETRY, range: {0, 0}} +│ +├─> LOCATE PHASE (apply_every with LocateHelper) +│ │ +│ ├─> LocateHelper on Driver (Next) +│ │ ├─> queue_.empty()? NO +│ │ ├─> oldest_stamp() = 2 +│ │ ├─> Set range = {lower: 2, upper: 2} +│ │ └─> Return (PRIMED, ExtractionRange{0, 1}) +│ │ +│ ├─> LocateHelper on Follower1 (ClosestBefore) +│ │ ├─> state == PRIMED? YES, continue +│ │ ├─> boundary = 2, period = 1 +│ │ ├─> Search window [1, 2) +│ │ ├─> Find stamp=1 ✓ +│ │ └─> Return (PRIMED, ExtractionRange{1, 2}) +│ │ +│ └─> LocateHelper on Follower2 (Before) +│ ├─> state == PRIMED? YES, continue +│ ├─> boundary = 2 - 1 = 1 (non-inclusive) +│ ├─> newest_stamp=15 >= 1? YES ✓ +│ ├─> Collect stamps < 1: [-2, -1, 0] +│ └─> Return (PRIMED, ExtractionRange{0, 3}) +│ +├─> All PRIMED → Proceed to EXTRACT +│ +├─> EXTRACT PHASE (apply_every_r with ExtractHelper) +│ │ +│ ├─> ExtractHelper on Driver +│ │ ├─> Move element at [0,1) to output +│ │ ├─> Remove first 1 element +│ │ └─> Output: [Dispatch{2, ...}] +│ │ +│ ├─> ExtractHelper on Follower1 +│ │ ├─> Move element at [1,2) to output +│ │ ├─> Remove first 2 elements +│ │ └─> Output: [Dispatch{1, ...}] +│ │ +│ └─> ExtractHelper on Follower2 +│ ├─> Move elements at [0,3) to output +│ ├─> Remove first 3 elements +│ └─> Output: [Dispatch{-2,...}, Dispatch{-1,...}, Dispatch{0,...}] +│ +└─> Return Result{PRIMED, range{2, 2}} + +FINAL STATE: + Driver queue: stamps [3, 4, ..., 19] + Follower1 queue: stamps [2, 3, ..., 17] + Follower2 queue: stamps [1, 2, ..., 15] +``` + +--- + +## 8. Configuration and Customization + +### Compile-Time Configuration + +Flow uses **template parameters** for configuration (zero runtime overhead): + +```cpp +// Threading model +using ST = driver::Next, NoLock>; // Single-threaded +using MT = driver::Next, std::unique_lock>; // Multi-threaded + +// Custom container +using CustomDriver = driver::Next< + MyDispatch, + NoLock, + std::list // Instead of default std::deque +>; + +// Custom allocator +using AllocDriver = driver::Next< + MyDispatch, + NoLock, + std::deque> +>; +``` + +### Custom Dispatch Types + +Adapt your existing data types: + +```cpp +// Your existing type +struct SensorMsg { + ros::Time timestamp; + SensorData data; +}; + +// Specialize Flow traits +namespace flow { + template<> struct DispatchTraits { + using stamp_type = ros::Time; + using value_type = SensorData; + }; + + template<> struct DispatchAccess { + static const ros::Time& stamp(const SensorMsg& m) { + return m.timestamp; + } + static const SensorData& value(const SensorMsg& m) { + return m.data; + } + }; + + // For non-integral stamps + template<> struct StampTraits { + using stamp_type = ros::Time; + using offset_type = ros::Duration; + static constexpr ros::Time min() { return ros::Time(0); } + static constexpr ros::Time max() { return ros::Time(INT_MAX); } + }; +} + +// Now use directly! +driver::Next sensor_driver; +``` + +### Queue Monitors + +Custom capture preconditioning: + +```cpp +struct MinSizeMonitor { + template + bool check(DispatchQueue& queue, + const CaptureRange& range) { + return queue.size() >= 10; // Require minimum 10 elements + } + + template + void update(DispatchQueue& queue, + const CaptureRange& range, State state) { + // Track statistics, etc. + } +}; + +// Use custom monitor +follower::Before, MinSizeMonitor> + follower{5, {}, MinSizeMonitor{}}; +``` + +--- + +## 9. Error Handling + +Flow uses **return codes** (no exceptions) for real-time compatibility: + +```cpp +auto [result, outputs] = Synchronizer::capture( + std::forward_as_tuple(driver, follower), + std::forward_as_tuple(d_out, f_out) +); + +switch (result.state) { + case State::PRIMED: + // ✅ Success - process data + process(d_out, f_out); + break; + + case State::RETRY: + // ⏳ Need more data - queue more and retry + break; + + case State::ABORT: + // ❌ Cannot synchronize - frame dropped automatically + break; + + case State::TIMEOUT: + // ⏱️ Multi-threaded wait expired + break; + + case State::ERROR_DRIVER_LOWER_BOUND_EXCEEDED: + // 🚨 CRITICAL: Timestamp went backwards! + handle_error(); + break; + + case State::SKIP_FRAME_QUEUE_PRECONDITION: + // ⏭️ Queue monitor rejected - continue + break; +} + +// Shorthand: boolean conversion checks for PRIMED +if (result) { + process(d_out, f_out); +} +``` + +**Compile-Time Errors:** +```cpp +// Type mismatch detected at compile-time: +driver::Next, NoLock> int_driver; +follower::Before, NoLock> double_follower{1}; + +// ERROR: "Associated captor stamp types do not match" +Synchronizer::capture(std::forward_as_tuple(int_driver, double_follower), ...); +``` + +--- + +## 10. Building and Testing + +### CMake Build + +```bash +# Configure with tests +cmake -DBUILD_TESTS=ON . + +# Build +make -j$(nproc) + +# Run all tests +ctest --output-on-failure + +# Run specific test +./build/synchronizer_st_example +``` + +### Bazel Build + +```bash +# Run all tests +bazel test test/... --test_output=all + +# Run specific test +bazel test test:synchronizer_st_example --test_output=all + +# Build modes: debug, sanitized, optimized +bazel test test/... --config=debug +``` + +### Code Formatting + +```bash +# Install tools +sudo apt install clang-format-7 python-pip +pip install pre-commit + +# Manual formatting +pre-commit run -a + +# Automatic on commit +pre-commit install +``` + +--- + +## 11. Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines. + +**Quick Start:** + +1. Fork and clone the repository +2. Create a feature branch +3. Make changes following C++14 standards +4. Run `pre-commit run -a` for formatting +5. Add/update tests in `test/flow/` +6. Submit pull request + +**Style Requirements:** +- clang-format-7 formatting (enforced by CI) +- `-Werror -Wall -Wextra` must pass +- All tests must pass + +--- + +## Quick Reference Card + +```cpp +// Include everything +#include + +// Define dispatch type +template +using MyDispatch = flow::Dispatch; + +// Create captors +flow::driver::Next, flow::NoLock> driver; +flow::follower::Before, flow::NoLock> follower{5}; + +// Inject data +driver.inject(timestamp, value); +follower.inject(timestamp, value); + +// Synchronize +auto [result, _] = flow::Synchronizer::capture( + std::forward_as_tuple(driver, follower), + std::forward_as_tuple(std::back_inserter(d), std::back_inserter(f)) +); + +// Check result +if (result.state == flow::State::PRIMED) { + // Process synchronized data in d and f +} + +// Reset all captors +flow::Synchronizer::reset(std::forward_as_tuple(driver, follower)); +``` + +--- + +**License:** MIT License - Copyright (c) 2020 Fetch Robotics Inc. + +**Documentation:** https://fetchrobotics.github.io/flow/doxygen-out/html/index.html + +**Related:** [Flow-ROS](https://github.com/fetchrobotics/flow_ros) wrapper library diff --git a/doc/usage_docs/README.md b/doc/usage_docs/README.md new file mode 100644 index 00000000..fcae40ad --- /dev/null +++ b/doc/usage_docs/README.md @@ -0,0 +1,86 @@ +# Flow Execution Examples + +This directory contains detailed execution examples demonstrating how Flow's capture mechanisms work with various driver and follower combinations. These examples are designed to help developers understand: + +- How data flows through the synchronization pipeline +- When and why captures succeed, retry, or fail +- Edge cases and potential pitfalls +- Message dropping scenarios and how to avoid them + +## Document Index + +| Document | Description | +|----------|-------------| +| [Driver Examples](./drivers.md) | Detailed examples for all driver types (Next, Batch, Chunk, Throttled) | +| [Follower Examples](./followers.md) | Detailed examples for all follower types | +| [Combined Workflows](./combined_workflows.md) | Real-world scenarios combining drivers and followers | +| [Edge Cases & Failures](./edge_cases_and_failures.md) | Common failure scenarios and how to handle them | +| [Message Dropping Scenarios](./message_dropping.md) | When and why messages get dropped | +| [Delay Configuration](./delay_configuration.md) | How delay parameter works, configuration options, and use cases | +| [Boundary Calculation Reference](./boundary_calculation.md) | **Important:** Clarifies `lower_stamp` vs `upper_stamp` usage in followers (includes documentation discrepancy notes) | +| [Before vs ClosestBefore](./before_vs_closest_before.md) | Detailed comparison with robotics use cases for choosing between these two followers | + +## Quick Reference: State Outcomes + +| State | Meaning | Action | +|-------|---------|--------| +| `PRIMED` | Capture successful | Process data | +| `RETRY` | Need more data | Wait and try again | +| `ABORT` | Cannot synchronize | Frame dropped, continue | +| `TIMEOUT` | Wait expired (MT only) | Retry or handle timeout | +| `ERROR_DRIVER_LOWER_BOUND_EXCEEDED` | Timestamp violation | Critical error | +| `SKIP_FRAME_QUEUE_PRECONDITION` | Queue monitor rejected | Continue to next frame | + +## Visual Legend + +In the execution traces, we use the following notation: + +``` +Queue State: [oldest] ← ← ← [newest] + [0, 1, 2, 3, 4, 5] + ↑ + Elements ordered by timestamp + +Capture Range: {lower_stamp, upper_stamp} + {10, 15} means timestamps 10 to 15 + +Timeline: + t=0 t=5 t=10 t=15 t=20 + |-----|-----|------|------| + + ▼ = captured element + ✗ = removed element + ○ = retained element +``` + +## Common Patterns + +### Pattern 1: Frame-by-Frame Processing +```cpp +driver::Next + follower::ClosestBefore +// Use when: Processing data one frame at a time with nearest-neighbor matching +``` + +### Pattern 2: Sliding Window Analysis +```cpp +driver::Batch + follower::Ranged +// Use when: Need overlapping windows for moving average, etc. +``` + +### Pattern 3: Non-Overlapping Batch Processing +```cpp +driver::Chunk + follower::Before +// Use when: Processing fixed-size batches without overlap +``` + +### Pattern 4: Rate-Limited Processing +```cpp +driver::Throttled + follower::Latched +// Use when: Input rate higher than processing rate, need stable state +``` + +### Pattern 5: Optional Data Streams +```cpp +driver::Next + follower::AnyBefore +// Use when: Some data streams may not always have data +``` diff --git a/doc/usage_docs/before_vs_closest_before.md b/doc/usage_docs/before_vs_closest_before.md new file mode 100644 index 00000000..c133cee7 --- /dev/null +++ b/doc/usage_docs/before_vs_closest_before.md @@ -0,0 +1,497 @@ +# Before vs ClosestBefore: Use Case Comparison + +This document provides a detailed comparison of `follower::Before` and `follower::ClosestBefore` to help you choose the right captor for your robotics application. + +--- + +## Table of Contents + +1. [Quick Comparison](#quick-comparison) +2. [Core Differences](#core-differences) +3. [Robotics Use Cases](#robotics-use-cases) +4. [Decision Flowchart](#decision-flowchart) +5. [Code Examples](#code-examples) +6. [Performance Considerations](#performance-considerations) +7. [Common Mistakes](#common-mistakes) + +--- + +## Quick Comparison + +| Aspect | `Before` | `ClosestBefore` | +|--------|----------|-----------------| +| **Output** | All elements before boundary | Single element closest to boundary | +| **Count** | 0 to N elements | Exactly 0 or 1 element | +| **Boundary** | `upper_stamp - delay` | `lower_stamp - delay` | +| **Staleness Check** | No (takes any data) | Yes (rejects if outside `period`) | +| **Fails if** | No proof element exists | Data too stale OR gap in window | +| **Parameters** | `delay` | `period`, `delay` | + +--- + +## Core Differences + +### Difference 1: Quantity of Data + +``` +Sensor data: [60, 70, 80, 85, 90, 95, 98, 102, 110] +Driver at t=100 + +BEFORE (boundary = 100): +┌─────────────────────────────────────────────────────┐ +│ Captures: [60, 70, 80, 85, 90, 95, 98] │ +│ Count: 7 elements │ +│ "Everything that happened before" │ +└─────────────────────────────────────────────────────┘ + +CLOSEST_BEFORE (boundary = 100, period = 20): +┌─────────────────────────────────────────────────────┐ +│ Window: [80, 100) │ +│ Captures: [98] (closest to 100 within window) │ +│ Count: 1 element │ +│ "Most recent state" │ +└─────────────────────────────────────────────────────┘ +``` + +### Difference 2: Data Freshness Enforcement + +``` +Sensor data: [10, 20, 30] ← All old data! +Driver at t=100 + +BEFORE: +┌─────────────────────────────────────────────────────┐ +│ boundary = 100 │ +│ Captures: [10, 20, 30] ← Takes stale data! │ +│ State: PRIMED (if proof element exists) │ +│ │ +│ ⚠️ No freshness check - accepts ANY old data │ +└─────────────────────────────────────────────────────┘ + +CLOSEST_BEFORE (period = 20): +┌─────────────────────────────────────────────────────┐ +│ boundary = 100 │ +│ Window: [80, 100) │ +│ Data in window: NONE (30 < 80) │ +│ State: ABORT │ +│ │ +│ ✓ Rejects stale data - enforces freshness │ +└─────────────────────────────────────────────────────┘ +``` + +### Difference 3: Failure Modes + +``` +BEFORE fails (RETRY) when: +├─ Queue is empty +└─ No "proof" element >= boundary exists + (Cannot prove all data has arrived) + +CLOSEST_BEFORE fails when: +├─ RETRY: No data in queue at all +├─ ABORT: Oldest element is >= boundary (gap in data) +└─ ABORT: No element in [boundary-period, boundary) window +``` + +--- + +## Robotics Use Cases + +### Use Case 1: IMU Integration (Use `Before`) + +**Scenario:** Visual-Inertial Odometry needs ALL IMU readings between camera frames. + +``` +Camera: 30fps → frames at t=0, 33, 66, 100, 133... +IMU: 200Hz → readings at t=0, 5, 10, 15, 20, 25, 30, 33, 38... + +Between camera frames at t=66 and t=100: +IMU readings: [66, 71, 76, 81, 86, 91, 96] + +VIO Algorithm needs to INTEGRATE all 7 readings: + delta_rotation = ∫ gyro dt + delta_velocity = ∫ accel dt + delta_position = ∫∫ accel dt² +``` + +**Why `Before`:** +- Need ALL data, not just one +- Missing any IMU reading corrupts the integration +- Output is variable-length (depends on timing) + +```cpp +driver::Next camera; +follower::Before imu{0}; + +// Each camera frame captures ~6-7 IMU readings +// [imu_66, imu_71, imu_76, imu_81, imu_86, imu_91, imu_96] +``` + +--- + +### Use Case 2: Transform Lookup (Use `ClosestBefore`) + +**Scenario:** Need robot pose when camera image was captured. + +``` +Camera: 30fps → image at t=100 +Odometry: 100Hz → poses at t=90, 100, 110... + +Question: "Where was the robot at t=100?" +Answer: Use pose closest to t=100 +``` + +**Why `ClosestBefore`:** +- Need exactly ONE pose +- Want the MOST RECENT (closest) pose +- Stale pose is dangerous (robot moved!) + +```cpp +driver::Next camera; +follower::ClosestBefore pose{15ms, 0}; +// period=15ms: pose must be within 15ms of image +// If pose is older, ABORT (safety!) + +// Captures: single pose at t=98 or t=100 +``` + +--- + +### Use Case 3: Event Logging (Use `Before`) + +**Scenario:** Log all warnings/errors that occurred before a robot action. + +``` +Robot action at t=1000 +Warning events: [800, 850, 920, 980] + +Question: "What warnings happened before the action?" +Answer: All of them: [800, 850, 920, 980] +``` + +**Why `Before`:** +- Need complete history +- Variable number of events +- No event is "more important" than another + +```cpp +driver::Next actions; +follower::Before warnings{0}; + +// Captures ALL warnings before the action +// Could be 0, could be 100 - depends on what happened +``` + +--- + +### Use Case 4: Lidar-Camera Calibration (Use `ClosestBefore`) + +**Scenario:** For each camera frame, get the corresponding LiDAR scan. + +``` +Camera: 30fps → frames at t=100, 133, 166... +LiDAR: 10fps → scans at t=50, 150, 250... + +For camera at t=100: + Closest LiDAR: t=50 (but 50ms old!) + +For camera at t=166: + Closest LiDAR: t=150 (16ms old - acceptable) +``` + +**Why `ClosestBefore`:** +- Need single scan to match with single frame +- Freshness matters (stale scan = wrong geometry) +- Period parameter rejects too-old data + +```cpp +driver::Next camera; +follower::ClosestBefore lidar{50ms, 0}; +// period=50ms: scan must be within 50ms + +// t=100: Would ABORT (scan 50 is 50ms old, edge case) +// t=166: Captures scan at t=150 (16ms old - OK) +``` + +--- + +### Use Case 5: Sensor Diagnostics (Use `Before`) + +**Scenario:** Collect all sensor health messages for monitoring dashboard. + +``` +Dashboard update at t=1000 +Health messages: [920: "temp=45C", 950: "voltage=12.1V", 980: "current=2.3A"] + +Dashboard needs: ALL health metrics, not just one +``` + +**Why `Before`:** +- Multiple independent metrics +- Need complete picture +- No single "closest" metric makes sense + +```cpp +driver::Throttled dashboard{1000ms}; +follower::Before health{0}; + +// Captures all health messages since last update +``` + +--- + +### Use Case 6: Wheel Odometry for Motion Model (Use `ClosestBefore`) + +**Scenario:** Localization particle filter needs odometry delta. + +``` +Localization runs at: 20Hz → t=50, 100, 150... +Wheel odometry at: 50Hz → t=40, 60, 80, 100, 120... + +For localization at t=100: + Need: single odometry reading to compute delta + Best: reading at t=100 (exact match!) + Acceptable: reading at t=80 (20ms old) +``` + +**Why `ClosestBefore`:** +- Need single pose to compute delta from last +- Stale odometry = wrong motion model +- Period rejects unacceptably old data + +```cpp +driver::Next localization; +follower::ClosestBefore odom{25ms, 0}; +// Must have odometry within 25ms +``` + +--- + +## Decision Flowchart + +``` + ┌─────────────────────────┐ + │ How many elements do │ + │ you need from this │ + │ follower stream? │ + └───────────┬─────────────┘ + │ + ┌───────────────┴───────────────┐ + │ │ + ▼ ▼ + ┌───────────────┐ ┌───────────────┐ + │ ALL elements │ │ ONE element │ + │ (variable N) │ │ (exactly 1) │ + └───────┬───────┘ └───────┬───────┘ + │ │ + ▼ ▼ + ┌───────────────┐ ┌───────────────────┐ + │ Does data │ │ Does freshness │ + │ freshness │ │ matter? │ + │ matter? │ │ (reject stale) │ + └───────┬───────┘ └─────────┬─────────┘ + │ │ + ┌───────┴───────┐ ┌───────┴───────┐ + │ │ │ │ + ▼ ▼ ▼ ▼ + ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ + │ Yes │ │ No │ │ Yes │ │ No │ + └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ + │ │ │ │ + ▼ ▼ ▼ ▼ + Consider ┌────────┐ ┌─────────────┐ Consider + CountBefore │ BEFORE │ │CLOSEST_BEFORE│ Latched + or custom └────────┘ └─────────────┘ +``` + +--- + +## Code Examples + +### Example 1: Visual-Inertial Odometry + +```cpp +#include + +// Camera drives the pipeline +driver::Next, NoLock> camera; + +// Need ALL IMU readings for preintegration +follower::Before, NoLock> imu{0}; + +// Need SINGLE pose at frame capture time +follower::ClosestBefore, NoLock> pose{ + 20, // period: pose must be within 20ms + 0 // delay: no offset +}; + +Synchronizer sync{camera, imu, pose}; + +void process() { + std::vector> cam_data; + std::vector> imu_data; // Multiple! + std::vector> pose_data; // Single! + + State state = sync.capture( + std::back_inserter(cam_data), + std::back_inserter(imu_data), + std::back_inserter(pose_data) + ); + + if (state == State::PRIMED) { + // cam_data: 1 frame + // imu_data: ~6-7 IMU readings (variable!) + // pose_data: 1 pose + + // Integrate IMU + ImuDelta delta = preintegrate(imu_data); + + // Run VIO with frame, IMU delta, and pose prior + run_vio(cam_data[0], delta, pose_data[0]); + } +} +``` + +### Example 2: Multi-Sensor Logging + +```cpp +// Log entry triggers capture +driver::Throttled, NoLock> logger{1000}; // 1Hz + +// Collect ALL events since last log +follower::Before, NoLock> events{0}; + +// Get LATEST system status +follower::ClosestBefore, NoLock> status{ + 500, // Must have status within 500ms + 0 +}; + +void log_iteration() { + auto [trigger, events, status] = capture(); + + // events: could be 0, 10, or 100 events + // status: exactly 1 status reading + + write_log_entry(trigger.stamp, events, status); +} +``` + +--- + +## Performance Considerations + +### Memory Usage + +``` +Before: +├─ Stores variable number of elements +├─ Memory: O(N) where N = data rate × time between captures +└─ Example: 200Hz IMU, 30Hz camera → ~7 elements/capture + +ClosestBefore: +├─ Stores exactly 1 element +├─ Memory: O(1) +└─ Always predictable +``` + +### Processing Time + +``` +Before: +├─ Must iterate to find all elements < boundary +├─ Processing: O(N) per capture +└─ Must process variable-length output + +ClosestBefore: +├─ Iterates to find single closest element +├─ Processing: O(N) to find, but O(1) output +└─ Fixed processing for output +``` + +### When Performance Matters + +```cpp +// High-rate system: 1000Hz driver, 10000Hz follower + +// BEFORE: Captures ~10 elements per iteration +// 1000 iterations/sec × 10 elements = 10,000 elements/sec processed + +// CLOSEST_BEFORE: Captures 1 element per iteration +// 1000 iterations/sec × 1 element = 1,000 elements/sec processed + +// If you only NEED the latest value, ClosestBefore is 10x more efficient! +``` + +--- + +## Common Mistakes + +### Mistake 1: Using Before When You Only Need One Value + +```cpp +// WRONG: Captures all poses, then only uses last one +follower::Before pose{0}; +auto poses = capture_poses(); +use_pose(poses.back()); // Wasted effort getting all others! + +// RIGHT: Directly get the one you need +follower::ClosestBefore pose{20, 0}; +auto pose = capture_pose(); // Just one! +use_pose(pose); +``` + +### Mistake 2: Using ClosestBefore When You Need History + +```cpp +// WRONG: Only gets one IMU, integration is incorrect! +follower::ClosestBefore imu{10, 0}; +auto imu = capture_imu(); // Missing 6 other readings! +delta = integrate(imu); // Huge error! + +// RIGHT: Get all IMU readings for proper integration +follower::Before imu{0}; +auto imus = capture_imus(); // All 7 readings +delta = integrate(imus); // Correct! +``` + +### Mistake 3: Ignoring Staleness Requirements + +```cpp +// WRONG: Accepts arbitrarily old pose data +follower::Before pose{0}; +// Could return pose from 10 seconds ago if stream stalled! + +// RIGHT: Fail if pose is too old +follower::ClosestBefore pose{100, 0}; // Max 100ms old +// ABORTs if pose older than 100ms - safer! +``` + +### Mistake 4: Period Too Small in ClosestBefore + +```cpp +// WRONG: Period smaller than data interval +// Data arrives every 10ms, but period is 5ms +follower::ClosestBefore data{5, 0}; // Window [boundary-5, boundary) +// Frequently ABORTs because window misses data! + +// RIGHT: Period >= data interval +follower::ClosestBefore data{15, 0}; // Window [boundary-15, boundary) +// Reliably captures data +``` + +--- + +## Summary + +| Question | Answer | +|----------|--------| +| Need ALL historical data? | Use `Before` | +| Need just the LATEST value? | Use `ClosestBefore` | +| Data freshness critical? | Use `ClosestBefore` (has period check) | +| Integrating over time? | Use `Before` | +| Looking up state at a moment? | Use `ClosestBefore` | +| Variable output size OK? | Use `Before` | +| Need predictable output size? | Use `ClosestBefore` | +| Don't care about stale data? | Use `Before` | +| Stale data is dangerous? | Use `ClosestBefore` | diff --git a/doc/usage_docs/boundary_calculation.md b/doc/usage_docs/boundary_calculation.md new file mode 100644 index 00000000..fbd3c59d --- /dev/null +++ b/doc/usage_docs/boundary_calculation.md @@ -0,0 +1,239 @@ +# Boundary Calculation Reference + +This document clarifies how each follower calculates its capture boundary, based on analysis of the actual implementation code. + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Documentation vs Implementation Discrepancy](#documentation-vs-implementation-discrepancy) +3. [Actual Boundary Calculations](#actual-boundary-calculations) +4. [Why the Difference Matters](#why-the-difference-matters) +5. [Practical Implications](#practical-implications) + +--- + +## Overview + +Each follower type calculates a **boundary** that determines which elements to capture. The boundary is calculated from the driver's `CaptureRange`, which contains: + +```cpp +struct CaptureRange { + StampT lower_stamp; // Oldest timestamp in driver's capture + StampT upper_stamp; // Newest timestamp in driver's capture +}; +``` + +For `driver::Next`, these are equal (`lower_stamp == upper_stamp`). +For `driver::Batch` or `driver::Chunk`, they differ. + +--- + +## Documentation vs Implementation Discrepancy + +⚠️ **Important Finding:** The README.md documentation states that most followers use `lower_stamp`, but the actual implementation uses `upper_stamp` for several followers. + +| Follower | README Says | Actual Code | +|----------|-------------|-------------| +| `Before` | `lower_stamp - delay` | **`upper_stamp - delay`** | +| `AnyBefore` | `lower_stamp - delay` | **`upper_stamp - delay`** | +| `AnyAtOrBefore` | `lower_stamp - delay` | **`upper_stamp - delay`** | +| `CountBefore` | `lower_stamp - delay` | **`upper_stamp - delay`** | +| `ClosestBefore` | `lower_stamp - delay` | `lower_stamp - delay` ✓ | +| `Latched` | `lower_stamp - min_period` | `lower_stamp - min_period` ✓ | +| `MatchedStamp` | `lower_stamp` | `lower_stamp` ✓ | +| `Ranged` | Both stamps | Both stamps ✓ | + +--- + +## Actual Boundary Calculations + +Based on the implementation in `include/flow/impl/follower/*.hpp`: + +### Followers Using `upper_stamp` + +#### Before +```cpp +// include/flow/impl/follower/before.hpp, line 55 +const stamp_type boundary = range.upper_stamp - delay_; +``` +Captures: All elements with `stamp < boundary` + +#### AnyBefore +```cpp +// include/flow/impl/follower/any_before.hpp, line 48 +const stamp_type boundary = range.upper_stamp - delay_; +``` +Captures: All elements with `stamp < boundary` (or empty, always PRIMED) + +#### AnyAtOrBefore +```cpp +// include/flow/impl/follower/any_at_or_before.hpp, line 49 +const stamp_type boundary = range.upper_stamp - delay_; +``` +Captures: All elements with `stamp <= boundary` (or empty, always PRIMED) + +#### CountBefore +```cpp +// include/flow/impl/follower/count_before.hpp, line 61 +const stamp_type boundary = range.upper_stamp - delay_; +``` +Captures: N elements with `stamp < boundary` + +### Followers Using `lower_stamp` + +#### ClosestBefore +```cpp +// include/flow/impl/follower/closest_before.hpp, line 44 +const stamp_type boundary = range.lower_stamp - delay_; +``` +Captures: One element in window `[boundary - period, boundary)` + +#### Latched +```cpp +// include/flow/impl/follower/latched.hpp +const stamp_type boundary = range.lower_stamp - min_period_; +``` +Captures: Most recent element with `stamp <= boundary` + +#### MatchedStamp +```cpp +// Uses range.lower_stamp directly for exact match +``` +Captures: Element with `stamp == range.lower_stamp` + +### Followers Using Both Stamps + +#### Ranged +```cpp +// include/flow/impl/follower/ranged.hpp, lines 114, 135 +offset_lower_stamp = range.lower_stamp - delay_; +offset_upper_stamp = range.upper_stamp - delay_; +``` +Captures: Elements spanning `[lower - delay, upper - delay]` plus boundary elements + +--- + +## Why the Difference Matters + +### For `driver::Next` (Single Element) + +When the driver captures a single element: +``` +range = {lower_stamp=100, upper_stamp=100} +``` + +Both `lower_stamp` and `upper_stamp` are equal, so **it doesn't matter** which one is used. + +### For `driver::Batch` or `driver::Chunk` (Multiple Elements) + +When the driver captures a range of elements: +``` +range = {lower_stamp=100, upper_stamp=200} +``` + +Now the choice matters significantly: + +``` +Timeline: + 50 100 200 250 + |--------|----------------|-------| + ^ ^ + lower upper + + +Using upper_stamp (Before, AnyBefore, CountBefore): + boundary = 200 - delay + + Captures data up to timestamp 200 (END of driver range) + This INCLUDES data that occurred DURING the driver's capture period! + + +Using lower_stamp (ClosestBefore, Latched, MatchedStamp): + boundary = 100 - delay + + Captures data up to timestamp 100 (START of driver range) + This captures data from BEFORE the driver's capture period began. +``` + +### Visual Example + +``` +Driver (Batch): captures images at t=[100, 200] + +Follower data: [50, 75, 90, 110, 150, 180, 210, 250] + +BEFORE (boundary = upper = 200, delay = 0): + Captures: [50, 75, 90, 110, 150, 180] + ├─ Before driver started: 50, 75, 90 + └─ DURING driver capture: 110, 150, 180 ← Included! + +CLOSEST_BEFORE (boundary = lower = 100, delay = 0, period = 20): + Window: [80, 100) + Captures: [90] + └─ Only data from BEFORE driver started +``` + +--- + +## Practical Implications + +### When to Use Each Type + +| Use Case | Recommended Follower | Why | +|----------|---------------------|-----| +| "All context up to now" | `Before` | Uses `upper_stamp`, gets everything including during driver capture | +| "State when driver started" | `ClosestBefore`, `Latched` | Uses `lower_stamp`, gets state at beginning | +| "Exact synchronization" | `MatchedStamp` | Uses `lower_stamp` for exact match | +| "Optional data, any available" | `AnyBefore` | Uses `upper_stamp`, never blocks | +| "Historical count" | `CountBefore` | Uses `upper_stamp`, N most recent before end | +| "Interpolation bounds" | `Ranged` | Uses both, spans entire range | + +### Common Gotcha with Batch/Chunk Drivers + +If you're using `driver::Batch` or `driver::Chunk` and you want follower data from BEFORE the batch started (not during), use: +- `ClosestBefore` - for single element +- `Latched` - for persistent state +- `MatchedStamp` - for exact timestamp + +If you're okay with (or want) data from during the batch period, use: +- `Before` - for all elements +- `CountBefore` - for N elements +- `AnyBefore` / `AnyAtOrBefore` - for optional data + +### Example: Sensor Fusion with Batch Driver + +```cpp +// Driver captures 10 images at a time +driver::Batch, NoLock> camera{10}; + +// WRONG (if you want IMU state at batch START): +// Before uses upper_stamp, so you get IMU data from during the batch too +follower::Before, NoLock> imu{0}; + +// CORRECT (for IMU state at batch START): +// ClosestBefore uses lower_stamp +follower::ClosestBefore, NoLock> imu{10, 0}; +``` + +--- + +## Summary Table + +| Follower | Boundary Formula | Stamp Used | Semantic Meaning | +|----------|-----------------|------------|------------------| +| `Before` | `upper_stamp - delay` | upper | "Everything before the END" | +| `AnyBefore` | `upper_stamp - delay` | upper | "Anything before the END (optional)" | +| `AnyAtOrBefore` | `upper_stamp - delay` | upper | "Anything at/before the END (optional)" | +| `CountBefore` | `upper_stamp - delay` | upper | "N elements before the END" | +| `ClosestBefore` | `lower_stamp - delay` | lower | "Closest to the START" | +| `Latched` | `lower_stamp - min_period` | lower | "State at the START" | +| `MatchedStamp` | `lower_stamp` | lower | "Exact match at START" | +| `Ranged` | Both stamps - delay | both | "Spanning the entire range" | + +--- + +## Note on Documentation + +The main `README.md` in the repository states that followers use `lower_stamp`, but the actual implementation for `Before`, `AnyBefore`, `AnyAtOrBefore`, and `CountBefore` uses `upper_stamp`. This document reflects the **actual implementation behavior** as of the current codebase. diff --git a/doc/usage_docs/combined_workflows.md b/doc/usage_docs/combined_workflows.md new file mode 100644 index 00000000..0041ccc2 --- /dev/null +++ b/doc/usage_docs/combined_workflows.md @@ -0,0 +1,671 @@ +# Combined Driver-Follower Workflows + +This document demonstrates real-world synchronization workflows combining various driver and follower types. + +--- + +## Table of Contents + +1. [Workflow 1: Frame-by-Frame Sensor Fusion](#workflow-1-frame-by-frame-sensor-fusion) +2. [Workflow 2: Sliding Window Analysis](#workflow-2-sliding-window-analysis) +3. [Workflow 3: Rate-Limited Processing with State](#workflow-3-rate-limited-processing-with-state) +4. [Workflow 4: Exact Timestamp Synchronization](#workflow-4-exact-timestamp-synchronization) +5. [Workflow 5: Batch Processing with Optional Streams](#workflow-5-batch-processing-with-optional-streams) +6. [Workflow 6: Interpolation with Ranged Data](#workflow-6-interpolation-with-ranged-data) + +--- + +## Workflow 1: Frame-by-Frame Sensor Fusion + +**Scenario:** Fuse camera frames with IMU data and robot pose + +**Components:** +- `driver::Next` - Camera frames (driving stream) +- `follower::ClosestBefore` - IMU readings (high frequency) +- `follower::Latched` - Robot pose (slow updates) + +### Setup + +```cpp +using Stamp = int; // milliseconds + +// Camera at 30fps (33ms period) +driver::Next, NoLock> camera; + +// IMU at 100Hz (10ms period) +follower::ClosestBefore, NoLock> imu{ + 15, // period: slightly larger than IMU period + 0 // delay: no delay +}; + +// Pose updates at ~1Hz +follower::Latched, NoLock> pose{ + 500 // min_period: expect updates every ~500ms or slower +}; +``` + +### Execution Trace + +``` +TIME: t=1000ms + +DATA STATE: + Camera queue: [967, 1000, 1033] + IMU queue: [970, 980, 990, 1000, 1010, 1020] + Pose queue: [500, 1000] + Pose latched: Dispatch{500, pose_A} + +SYNCHRONIZATION: + + STEP 1: Driver (Camera) establishes range + ├─ camera.locate() + │ ├─ oldest_stamp() = 967 + │ └─ range = {967, 967} + │ + └─ State: PRIMED + + STEP 2: Follower 1 (IMU) - ClosestBefore + ├─ imu.locate(range={967, 967}) + │ ├─ boundary = 967 - 0 = 967 + │ ├─ window = [967-15, 967) = [952, 967) + │ │ + │ ├─ Find closest in window: + │ │ └─ No IMU stamp in [952, 967) + │ │ (closest before is 990, which is > 967!) + │ │ + │ └─ Wait... this would ABORT! + │ + │ ⚠️ PROBLEM: IMU data is AHEAD of camera! + │ Camera stamp 967 is older than IMU stamps. + │ + └─ State: ABORT + + → Frame dropped due to IMU data gap! + +NEXT SYNCHRONIZATION (after more data): + + Camera queue: [1000, 1033, 1067] + IMU queue: [980, 990, 1000, 1010, 1020, 1030] + + STEP 1: Driver range = {1000, 1000} + + STEP 2: IMU.locate(range={1000, 1000}) + ├─ boundary = 1000, window = [985, 1000) + ├─ Find closest: stamp 990 is in [985, 1000) ✓ + └─ State: PRIMED + + STEP 3: Pose.locate(range={1000, 1000}) + ├─ boundary = 1000 - 500 = 500 + ├─ Find stamp <= 500: stamp 500 ✓ + │ OR use latched value + └─ State: PRIMED + + EXTRACTION: + ├─ Camera: Dispatch{1000, frame} + ├─ IMU: Dispatch{990, imu_reading} + └─ Pose: Dispatch{500, pose_A} (latched) + + RESULT: State::PRIMED + + Synchronized output for t=1000: + - Camera frame from t=1000 + - IMU reading from t=990 (closest before) + - Robot pose from t=500 (latched, still valid) +``` + +### Key Insights + +1. **IMU Period Configuration**: Must be >= actual IMU period to avoid ABORTs +2. **Pose Latching**: Handles slow/sparse updates gracefully +3. **Timing Alignment**: All data selected relative to camera timestamp + +--- + +## Workflow 2: Sliding Window Analysis + +**Scenario:** Moving average over sensor data with historical context + +**Components:** +- `driver::Batch` - Main sensor (sliding window) +- `follower::CountBefore` - Fixed number of prior readings +- `follower::Before` - All auxiliary data in window + +### Setup + +```cpp +// Main sensor: capture 10 samples, slide by 1 +driver::Batch, NoLock> sensor{10}; + +// Temperature: always want last 5 readings before window +follower::CountBefore, NoLock> temp{ + 5, // count: exactly 5 elements + 0 // delay +}; + +// Events: all events before window start +follower::Before, NoLock> events{0}; +``` + +### Execution Trace + +``` +DATA STATE at t=100: + Sensor queue: [0,5,10,15,20,25,30,35,40,45,50,55,60,65,70] + Temp queue: [-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55] + Events queue: [2,18,33,67] + +CAPTURE 1: + + DRIVER (Batch{10}): + ├─ Capture: stamps [0,5,10,15,20,25,30,35,40,45] + ├─ Range: {0, 45} + └─ Queue after: [5,10,15,20,25,30,35,40,45,50,55,60,65,70] + (only stamp 0 removed) + + FOLLOWER 1 (CountBefore{5}): + ├─ boundary = 0 (range.lower_stamp - delay) + ├─ Need 5 elements with stamp < 0 + │ └─ Found: [-20,-15,-10,-5] = only 4 elements! + │ + └─ State: RETRY (need 1 more element < 0) + + OVERALL: State::RETRY + + → Sync fails because temp doesn't have enough historical data + +LATER - More temperature data arrives: + Temp queue: [-25,-20,-15,-10,-5,0,5,10,...,55] + +CAPTURE 2: + + DRIVER: + ├─ Queue: [5,10,15,20,25,30,35,40,45,50,55,60,65,70] + ├─ Capture: stamps [5,10,15,20,25,30,35,40,45,50] + ├─ Range: {5, 50} + └─ After: [10,15,20,25,30,35,40,45,50,55,60,65,70] + + FOLLOWER 1 (CountBefore{5}): + ├─ boundary = 5 + ├─ Elements < 5: [-25,-20,-15,-10,-5,0] = 6 elements + ├─ Select last 5: [-20,-15,-10,-5,0] + └─ State: PRIMED + + FOLLOWER 2 (Before): + ├─ boundary = 5 + ├─ Have element >= 5? Yes (stamp 5) + ├─ Elements < 5: stamps 2 (events) + └─ State: PRIMED (captures [Event{2}]) + + OVERALL: State::PRIMED + + OUTPUT: + ├─ Sensor: [5,10,15,20,25,30,35,40,45,50] (10 samples) + ├─ Temp: [-20,-15,-10,-5,0] (5 prior readings) + └─ Events: [Event{2}] (1 event before window) + +CAPTURE 3 (sliding window effect): + + DRIVER: + ├─ Queue: [10,15,20,25,30,35,40,45,50,55,60,65,70] + ├─ Capture: [10,15,20,25,30,35,40,45,50,55] + ├─ Range: {10, 55} + │ + │ Note: stamps [10..50] captured AGAIN (overlap!) + │ + └─ After: [15,20,25,30,35,40,45,50,55,60,65,70] + + OUTPUT: + ├─ Sensor: [10,15,20,25,30,35,40,45,50,55] + ├─ Temp: [-15,-10,-5,0,5] (shifted by 1) + └─ Events: [Event{2}] (same event, still < 10) +``` + +--- + +## Workflow 3: Rate-Limited Processing with State + +**Scenario:** High-frequency data with rate limiting and configuration state + +**Components:** +- `driver::Throttled` - High-frequency sensor (downsampled) +- `follower::Latched` - Configuration parameters +- `follower::AnyBefore` - Optional diagnostics + +### Setup + +```cpp +// Sensor at 1000Hz, process at 100Hz (throttle period = 10ms) +driver::Throttled, NoLock> sensor{10}; + +// Config updates infrequently +follower::Latched, NoLock> config{100}; + +// Optional diagnostic events +follower::AnyBefore, NoLock> diag{5}; +``` + +### Execution Trace + +``` +DATA INJECTION (1000Hz): + for (int t = 0; t < 100; ++t) { + sensor.inject(t, data); // t = 0,1,2,3,4,...,99 + } + + Sensor queue: [0,1,2,3,4,5,...,99] (100 elements) + Config queue: [0:"v1"] + Diag queue: [15:"warning", 45:"info", 78:"error"] + + Sensor.last_captured_ = MIN + +CAPTURE 1: + + DRIVER (Throttled{10}): + ├─ target = MIN + 10 ≈ MIN + ├─ First element >= MIN: stamp 0 + ├─ Range: {0, 0} + ├─ last_captured_ = 0 + └─ Remove: stamp 0 only + + Queue after: [1,2,3,4,...,99] + + FOLLOWER 1 (Latched): + ├─ boundary = 0 - 100 = -100 + ├─ Find stamp <= -100: NONE + ├─ latched_.has_value()? NO (first capture) + │ + │ But wait - stamp 0 is in queue! + │ Hmm, boundary -100 < all stamps + │ + └─ State: ABORT? + + ⚠️ PROBLEM: Latched min_period too large! + Config stamp 0 is > boundary -100 + But we need it for processing! + +SOLUTION: Adjust config min_period: + follower::Latched<...> config{0}; // min_period = 0 + +CAPTURE 1 (fixed): + + DRIVER: Range = {0, 0} + + FOLLOWER 1 (Latched{0}): + ├─ boundary = 0 - 0 = 0 + ├─ Find stamp <= 0: stamp 0 ✓ + └─ State: PRIMED, latched_ = Config{"v1"} + + FOLLOWER 2 (AnyBefore{5}): + ├─ boundary = 0 - 5 = -5 + ├─ Elements < -5: NONE + └─ State: PRIMED (empty, OK!) + + OUTPUT: + ├─ Sensor: [Dispatch{0}] + ├─ Config: [Dispatch{0, "v1"}] + └─ Diag: [] (empty) + +CAPTURE 2: + + DRIVER (Throttled): + ├─ target = 0 + 10 = 10 + ├─ Find first >= 10: stamp 10 + ├─ Range: {10, 10} + └─ Remove: stamps 1-10 + + DROPPED: stamps 1,2,3,4,5,6,7,8,9 (within throttle period) + + Queue after: [11,12,...,99] + + FOLLOWER 1 (Latched): + ├─ boundary = 10 + ├─ No new config in queue + └─ State: PRIMED (uses latched_ = "v1") + + FOLLOWER 2 (AnyBefore{5}): + ├─ boundary = 10 - 5 = 5 + ├─ Elements < 5: NONE (queue starts at 15) + │ + │ Wait - what about stamp 15 "warning"? + │ It's >= 5, so not captured. + │ + └─ State: PRIMED (empty) + + OUTPUT: + ├─ Sensor: [Dispatch{10}] + ├─ Config: [Dispatch{0, "v1"}] (latched) + └─ Diag: [] (warning at 15 not yet captured) + +CAPTURE 3: + + DRIVER: target = 20, captures stamp 20 + DROPPED: stamps 11-19 + + FOLLOWER 2 (AnyBefore): + ├─ boundary = 20 - 5 = 15 + ├─ Elements < 15: NONE (warning is AT 15) + │ + │ Note: AnyBefore is exclusive (< boundary) + │ + └─ State: PRIMED (empty) + + ⚠️ The warning at stamp 15 will be captured when boundary > 15 + +CAPTURE 4: + + DRIVER: target = 30, captures stamp 30 + + FOLLOWER 2: + ├─ boundary = 30 - 5 = 25 + ├─ Elements < 25: stamp 15 "warning" ✓ + └─ State: PRIMED + + OUTPUT: + └─ Diag: [Dispatch{15, "warning"}] + +FINAL STATE after all captures: + Captured sensor stamps: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 + Dropped sensor stamps: 1-9, 11-19, 21-29, ... (81 total dropped) + + Processing rate: 10 captures = 100Hz (as intended) + Data rate: 100 samples = 1000Hz + Drop rate: 90% +``` + +--- + +## Workflow 4: Exact Timestamp Synchronization + +**Scenario:** Multiple sensors with hardware-synchronized timestamps + +**Components:** +- `driver::Next` - Primary sensor +- `follower::MatchedStamp` - Secondary sensors (same timestamps) + +### Setup + +```cpp +// All sensors triggered by same hardware clock +driver::Next, NoLock> sensor_a; +follower::MatchedStamp, NoLock> sensor_b; +follower::MatchedStamp, NoLock> sensor_c; +``` + +### Execution Trace + +``` +PERFECT SYNCHRONIZATION: + + Sensor A: [100, 200, 300, 400] + Sensor B: [100, 200, 300, 400] // Same timestamps + Sensor C: [100, 200, 300, 400] + +CAPTURE 1: + DRIVER A: range = {100, 100} + FOLLOWER B: find stamp == 100 → PRIMED + FOLLOWER C: find stamp == 100 → PRIMED + + OUTPUT: + ├─ A: Dispatch{100, ...} + ├─ B: Dispatch{100, ...} + └─ C: Dispatch{100, ...} + + ✓ All three sensors perfectly aligned at t=100 + +CAPTURE 2, 3, 4: Similar... + + +IMPERFECT SYNCHRONIZATION (missing data): + + Sensor A: [100, 200, 300, 400] + Sensor B: [100, 200, 300, 400] + Sensor C: [100, 300, 400] // MISSING stamp 200! + +CAPTURE 1: range = {100, 100} + All PRIMED, output aligned at t=100 + +CAPTURE 2: range = {200, 200} + DRIVER A: PRIMED + FOLLOWER B: stamp 200 found → PRIMED + FOLLOWER C: + ├─ Find stamp == 200 + ├─ Queue: [300, 400] + ├─ oldest(300) > 200 + │ └─ Proves 200 will never arrive + │ + └─ State: ABORT! + + OVERALL: State::ABORT + + ⚠️ Frame at t=200 dropped due to missing sensor C data! + +CAPTURE 3: range = {300, 300} + All PRIMED, output aligned at t=300 + +RESULT: + - t=100: Synchronized ✓ + - t=200: DROPPED (sensor C missing) + - t=300: Synchronized ✓ + - t=400: Synchronized ✓ +``` + +### Handling Missing Data + +```cpp +// Option 1: Use AnyAtOrBefore for fault tolerance +follower::AnyAtOrBefore, NoLock> sensor_c{0}; +// Will return empty if stamp missing, but won't ABORT + +// Option 2: Use ClosestBefore with tight tolerance +follower::ClosestBefore, NoLock> sensor_c{ + 1, // period = 1 (expect exact match or ±1) + 0 // delay = 0 +}; +// Will find closest match, may return slightly off timestamp +``` + +--- + +## Workflow 5: Batch Processing with Optional Streams + +**Scenario:** Process data in chunks with optional auxiliary information + +**Components:** +- `driver::Chunk` - Main data batches +- `follower::Before` - Required context +- `follower::AnyBefore` - Optional metadata + +### Setup + +```cpp +// Process in batches of 100 +driver::Chunk, NoLock> data{100}; + +// Required: header info before each batch +follower::Before, NoLock> headers{0}; + +// Optional: annotations (may not exist) +follower::AnyBefore, NoLock> annotations{0}; +``` + +### Execution Trace + +``` +DATA STATE: + Data: [0..999] (1000 packets) + Headers: [0:"H1", 100:"H2", 200:"H3", ...] + Annotations: [50:"note1", 350:"note2"] // Sparse! + +CAPTURE 1: + DRIVER (Chunk{100}): + ├─ Capture: stamps [0..99] + ├─ Range: {0, 99} + └─ Remove all captured + + FOLLOWER 1 (Before): + ├─ boundary = 0 + ├─ Elements < 0: NONE + ├─ Have element >= 0? YES (stamp 0) + └─ State: PRIMED (empty capture, but valid) + + FOLLOWER 2 (AnyBefore): + ├─ boundary = 0 + ├─ Elements < 0: NONE + └─ State: PRIMED (always, empty OK) + + OUTPUT: + ├─ Data: [packets 0-99] + ├─ Headers: [] (none before 0) + └─ Annotations: [] (none before 0) + +CAPTURE 2: + DRIVER: + ├─ Range: {100, 199} + └─ Capture packets 100-199 + + FOLLOWER 1 (Before): + ├─ boundary = 100 + ├─ Elements < 100: [Header{0, "H1"}] + └─ State: PRIMED + + FOLLOWER 2 (AnyBefore): + ├─ boundary = 100 + ├─ Elements < 100: [Annotation{50, "note1"}] + └─ State: PRIMED + + OUTPUT: + ├─ Data: [packets 100-199] + ├─ Headers: [Header{0, "H1"}] + └─ Annotations: [Annotation{50, "note1"}] + +CAPTURE 3: + DRIVER: Range = {200, 299} + + FOLLOWER 1: + ├─ boundary = 200 + ├─ Elements < 200: [Header{100, "H2"}] + └─ State: PRIMED + + FOLLOWER 2: + ├─ boundary = 200 + ├─ Elements < 200: NONE (note1 already captured) + └─ State: PRIMED (empty) + + OUTPUT: + ├─ Data: [packets 200-299] + ├─ Headers: [Header{100, "H2"}] + └─ Annotations: [] // No annotations in this range + +CAPTURE 4: + DRIVER: Range = {300, 399} + + OUTPUT: + ├─ Data: [packets 300-399] + ├─ Headers: [Header{200, "H3"}] + └─ Annotations: [Annotation{350, "note2"}] + + ⚠️ Wait - annotation 350 is INSIDE the batch range [300, 399]! + But boundary = 300, so only elements < 300 captured. + Annotation 350 will be captured in NEXT batch. + +CAPTURE 5: + DRIVER: Range = {400, 499} + + OUTPUT: + └─ Annotations: [Annotation{350, "note2"}] // From previous range +``` + +--- + +## Workflow 6: Interpolation with Ranged Data + +**Scenario:** Get data points before and after for interpolation + +**Components:** +- `driver::Next` - Query timestamps +- `follower::Ranged` - Data for interpolation + +### Setup + +```cpp +// Query stream (where we need interpolated values) +driver::Next, NoLock> queries; + +// Data stream for interpolation (need before + after) +follower::Ranged, NoLock> data{0}; +``` + +### Execution Trace + +``` +DATA STATE: + Queries: [50, 150, 250] // Request values at these times + Data: [0, 100, 200, 300] // Known values at these times + +CAPTURE 1: + DRIVER: range = {50, 50} + + FOLLOWER (Ranged): + ├─ Need: 1 before 50, elements in [50,50], 1 after 50 + │ + ├─ Before 50: stamp 0 ✓ + ├─ In [50,50]: NONE (no exact match) + ├─ After 50: stamp 100 ✓ + │ + └─ State: PRIMED + + OUTPUT: + ├─ Query: Dispatch{50, query} + └─ Data: [Dispatch{0, v0}, Dispatch{100, v1}] + + Application can now interpolate: + value_at_50 = v0 + (v1 - v0) * (50 - 0) / (100 - 0) + = v0 + 0.5 * (v1 - v0) + +CAPTURE 2: + DRIVER: range = {150, 150} + + FOLLOWER: + ├─ Before 150: stamp 100 ✓ + ├─ After 150: stamp 200 ✓ + └─ State: PRIMED + + OUTPUT: + └─ Data: [Dispatch{100}, Dispatch{200}] + + Interpolation: value_at_150 = v100 + 0.5 * (v200 - v100) + +EDGE CASE - Query before all data: + Queries: [50] + Data: [100, 200, 300] // Nothing before 50! + + DRIVER: range = {50, 50} + + FOLLOWER: + ├─ Before 50: NONE! + └─ State: ABORT (cannot interpolate without lower bound) + +EDGE CASE - Query after all data: + Queries: [350] + Data: [100, 200, 300] // Nothing after 350! + + DRIVER: range = {350, 350} + + FOLLOWER: + ├─ Before 350: stamp 300 ✓ + ├─ After 350: NONE (newest is 300) + └─ State: RETRY (waiting for data after 350) +``` + +--- + +## Summary: Workflow Selection Guide + +| Scenario | Driver | Primary Follower | Secondary Followers | +|----------|--------|------------------|---------------------| +| Frame-by-frame | `Next` | `ClosestBefore` | `Latched`, `AnyBefore` | +| Sliding window | `Batch` | `Ranged`, `Before` | `CountBefore` | +| Fixed batches | `Chunk` | `Before` | `AnyBefore` | +| Rate limiting | `Throttled` | `Latched` | `AnyBefore` | +| Exact sync | `Next` | `MatchedStamp` | - | +| Interpolation | `Next` | `Ranged` | - | diff --git a/doc/usage_docs/delay_configuration.md b/doc/usage_docs/delay_configuration.md new file mode 100644 index 00000000..00d3a805 --- /dev/null +++ b/doc/usage_docs/delay_configuration.md @@ -0,0 +1,476 @@ +# Delay Configuration Guide + +This document explains how the `delay` parameter works in Flow followers, its configuration options, and practical use cases. + +--- + +## Table of Contents + +1. [What is Delay?](#what-is-delay) +2. [Delay in Each Follower Type](#delay-in-each-follower-type) +3. [Boundary Calculation](#boundary-calculation) +4. [Use Cases](#use-cases) +5. [Configuration Examples](#configuration-examples) +6. [Common Pitfalls](#common-pitfalls) + +--- + +## What is Delay? + +The `delay` parameter is an **offset** that shifts the capture boundary relative to the driver's timestamp range. It allows followers to look further back (or forward) in time when selecting which data to capture. + +``` +WITHOUT DELAY (delay = 0): + + Driver timestamp: |------ range -------| + lower upper + ↓ + Follower boundary: ─────────────────────● (at upper_stamp) + Captures: ████████████████████ + (everything before boundary) + +WITH DELAY (delay = 5): + + Driver timestamp: |------ range -------| + lower upper + ↓ + Follower boundary: ─────● (at upper_stamp - 5) + Captures: █████ + (everything before shifted boundary) +``` + +### Key Concept + +``` +boundary = driver_timestamp - delay + +A positive delay shifts the boundary EARLIER in time +A zero delay means the boundary equals the driver timestamp +``` + +--- + +## Delay in Each Follower Type + +All followers (except `MatchedStamp` and `Latched`) accept a `delay` parameter: + +### Constructor Signatures + +```cpp +// Before: captures all elements before boundary +follower::Before before{delay}; + +// ClosestBefore: captures closest element before boundary within period +follower::ClosestBefore closest{period, delay}; + +// CountBefore: captures N elements before boundary +follower::CountBefore count{n, delay}; + +// Ranged: captures elements in range with bounds +follower::Ranged ranged{delay}; + +// AnyBefore: optionally captures elements before boundary +follower::AnyBefore any_before{delay}; + +// AnyAtOrBefore: optionally captures elements at or before boundary +follower::AnyAtOrBefore any_at_before{delay}; +``` + +### Followers WITHOUT Delay Parameter + +```cpp +// MatchedStamp: requires exact timestamp match +follower::MatchedStamp matched{}; // No delay + +// Latched: uses min_period instead +follower::Latched latched{min_period}; // Different concept +``` + +--- + +## Boundary Calculation + +⚠️ **Important:** The README.md documentation states that most followers use `lower_stamp`, but the actual implementation differs. See [Boundary Calculation Reference](./boundary_calculation.md) for the full analysis. + +Different followers calculate their boundary differently: + +### Upper Stamp Based (Before, CountBefore, AnyBefore, AnyAtOrBefore) + +```cpp +// From include/flow/impl/follower/before.hpp (line 55) +const stamp_type boundary = range.upper_stamp - delay_; +``` + +``` +Driver range: {lower=10, upper=20} +delay = 5 + +boundary = 20 - 5 = 15 + +Captures elements with stamp < 15 +``` + +### Lower Stamp Based (ClosestBefore, Latched, MatchedStamp) + +```cpp +// From include/flow/impl/follower/closest_before.hpp (line 44) +const stamp_type boundary = range.lower_stamp - delay_; +``` + +``` +Driver range: {lower=10, upper=20} +delay = 5 + +boundary = 10 - 5 = 5 + +Searches for closest element before stamp 5 +``` + +### Both Stamps (Ranged) + +```cpp +// From include/flow/impl/follower/ranged.hpp +offset_lower_stamp = range.lower_stamp - delay_; +offset_upper_stamp = range.upper_stamp - delay_; +``` + +``` +Driver range: {lower=10, upper=20} +delay = 5 + +Shifted range: {lower=5, upper=15} + +Captures elements in [5, 15] plus boundary elements +``` + +### Why This Matters + +For `driver::Next` (single element), `lower_stamp == upper_stamp`, so it doesn't matter which is used. + +For `driver::Batch` or `driver::Chunk`: +- **`upper_stamp` followers** (`Before`, `AnyBefore`, `CountBefore`): Capture data up to the END of the driver range, including data from DURING the driver's capture period +- **`lower_stamp` followers** (`ClosestBefore`, `Latched`): Capture data up to the START of the driver range, only data from BEFORE the driver's capture period began + +--- + +## Use Cases + +### Use Case 1: Sensor Latency Compensation + +**Scenario:** Your IMU publishes data with timestamps 10ms behind the actual event time due to processing latency. + +```cpp +// Camera frames (driver) at actual time +driver::Next, NoLock> camera; + +// IMU data arrives 10ms late +// Without delay: IMU stamp 90 would be "closest to" camera stamp 100 +// With delay=10: looks for IMU data at stamp 90, which is correct! + +follower::ClosestBefore, NoLock> imu{ + 15, // period: expect data every ~15ms + 10 // delay: compensate for 10ms latency +}; +``` + +``` +TIMELINE: + Real event: t=100 (camera captures frame) + Camera stamp: t=100 + IMU event: t=100 (same real-world time) + IMU stamp: t=90 (delayed by 10ms in timestamp) + +WITHOUT DELAY (delay=0): + boundary = 100 - 0 = 100 + Looks for IMU near stamp 100 + May miss the correct IMU reading! + +WITH DELAY (delay=10): + boundary = 100 - 10 = 90 + Looks for IMU near stamp 90 + Correctly finds the synchronized IMU reading! +``` + +### Use Case 2: Look-Ahead Buffer + +**Scenario:** You need historical context before processing the current frame. + +```cpp +// Process frames but need 50ms of prior data for smoothing +driver::Next, NoLock> frames; + +follower::Before, NoLock> history{ + 50 // delay: capture data up to 50ms before frame +}; +``` + +``` +Frame at t=100: + boundary = 100 - 50 = 50 + Captures all sensor data with stamp < 50 + + Use case: Moving average, Kalman filter initialization +``` + +### Use Case 3: Time-Shifted Data Streams + +**Scenario:** Two sensors have known constant offset in their timestamp domains. + +```cpp +// Sensor A timestamps in system clock +driver::Next, NoLock> sensor_a; + +// Sensor B timestamps offset by +100ms from system clock +// (e.g., different clock source) +follower::ClosestBefore, NoLock> sensor_b{ + 20, // period + -100 // NEGATIVE delay: sensor B is 100ms AHEAD +}; +``` + +``` +Sensor A stamp: 1000 +Sensor B stamp: 1100 (same real-world time) + +boundary = 1000 - (-100) = 1100 + +Correctly aligns with Sensor B's timestamp domain! +``` + +### Use Case 4: Capturing Prior State + +**Scenario:** Robot needs the configuration that was active BEFORE the current command. + +```cpp +driver::Next, NoLock> commands; + +// Get config that was valid at least 1 second before command +follower::Latched, NoLock> config{1000}; // min_period +``` + +Note: `Latched` uses `min_period` instead of `delay`, but achieves similar effect: +```cpp +// From Latched implementation +boundary = range.lower_stamp - min_period_; +``` + +### Use Case 5: Interpolation Data Selection + +**Scenario:** Need data points surrounding a target time for interpolation. + +```cpp +driver::Next, NoLock> queries; + +// Get surrounding points for interpolation +// delay=0 means exact alignment with query timestamps +follower::Ranged, NoLock> samples{0}; +``` + +``` +Query at t=150: + delay = 0 + range = {150, 150} + + Ranged captures: + - One element before 150 (e.g., stamp 100) + - Elements in [150, 150] (if any) + - One element after 150 (e.g., stamp 200) + + Result: [100, 200] for linear interpolation +``` + +--- + +## Configuration Examples + +### Example 1: Basic Delay Setup + +```cpp +#include + +using namespace flow; +using Dispatch = Dispatch; + +int main() { + // Driver: process data one at a time + driver::Next driver; + + // Follower with 10-unit delay + follower::Before follower{10}; + + // Inject data + driver.inject(100, 1.0); + follower.inject(85, 0.85); // Will be captured (< 100-10=90) + follower.inject(92, 0.92); // Will NOT be captured (>= 90) + follower.inject(95, 0.95); // Will NOT be captured (>= 90) + + Synchronizer sync{driver, follower}; + + std::vector driver_data, follower_data; + State state = sync.capture( + std::back_inserter(driver_data), + std::back_inserter(follower_data) + ); + + // driver_data: [{100, 1.0}] + // follower_data: [{85, 0.85}] + + return 0; +} +``` + +### Example 2: ClosestBefore with Period and Delay + +```cpp +// IMU at 100Hz (10ms period), with 5ms timestamp delay +follower::ClosestBefore, NoLock> imu{ + 10, // period: maximum expected gap between readings + 5 // delay: timestamp offset compensation +}; + +// Camera frame at t=1000 +// boundary = 1000 - 5 = 995 +// window = [995 - 10, 995) = [985, 995) +// Captures closest IMU reading in [985, 995) +``` + +### Example 3: CountBefore with Delay + +```cpp +// Need exactly 5 historical readings, with 20-unit lookback +follower::CountBefore, NoLock> history{ + 5, // count: exactly 5 elements + 20 // delay: look 20 units back +}; + +// Driver at t=100 +// boundary = 100 - 20 = 80 +// Captures 5 most recent elements with stamp < 80 +``` + +### Example 4: Zero Delay (Common Case) + +```cpp +// When timestamps are already aligned, use delay=0 +follower::Before, NoLock> aligned{0}; + +// boundary = driver_upper_stamp - 0 = driver_upper_stamp +// Captures everything strictly before driver timestamp +``` + +--- + +## Common Pitfalls + +### Pitfall 1: Delay Too Large + +```cpp +follower::Before, NoLock> follower{1000}; // Large delay +``` + +``` +Driver at t=100: + boundary = 100 - 1000 = -900 + + Follower queue: [50, 75, 90, 110] + + Elements < -900: NONE + + ⚠️ Always captures empty! Delay is too large for the data. +``` + +**Fix:** Match delay to actual timing relationship between streams. + +### Pitfall 2: Negative Delay Confusion + +```cpp +// Negative delay shifts boundary FORWARD in time +follower::Before, NoLock> follower{-50}; +``` + +``` +Driver at t=100: + boundary = 100 - (-50) = 150 + + Follower queue: [50, 75, 90, 110, 130] + + Elements < 150: [50, 75, 90, 110, 130] + + ⚠️ Captures MORE data than expected! + ⚠️ May capture data "from the future" relative to driver! +``` + +**Caution:** Negative delay is valid but can lead to unexpected behavior. + +### Pitfall 3: Period vs Delay Confusion (ClosestBefore) + +```cpp +follower::ClosestBefore, NoLock> follower{ + 5, // period (window size) + 10 // delay (boundary offset) +}; +``` + +``` +These are DIFFERENT concepts: +- period: Size of the search window +- delay: Offset of the boundary + +Driver at t=100: + boundary = 100 - 10 = 90 + window = [90 - 5, 90) = [85, 90) + + Looks for closest element in [85, 90) +``` + +### Pitfall 4: Inconsistent Delay Across Followers + +```cpp +// PROBLEM: Different delays cause misalignment +follower::ClosestBefore<...> imu{10, 5}; // delay=5 +follower::Before<...> events{20}; // delay=20 + +// At driver stamp 100: +// IMU boundary: 100 - 5 = 95 +// Events boundary: 100 - 20 = 80 +// +// These capture data from different time periods! +``` + +**Fix:** Use consistent delays unless streams have different timing relationships. + +### Pitfall 5: Forgetting Delay in Latched + +```cpp +// Latched uses min_period, NOT delay +follower::Latched, NoLock> config{100}; + +// This is NOT the same as delay! +// min_period affects how far back to look for valid config +// boundary = range.lower_stamp - min_period +``` + +--- + +## Summary: Delay Parameter Reference + +| Follower | Delay Used In | Effect | +|----------|--------------|--------| +| `Before` | `upper_stamp - delay` | Captures elements before shifted boundary | +| `ClosestBefore` | `lower_stamp - delay` | Finds closest element before shifted boundary | +| `CountBefore` | `upper_stamp - delay` | Counts elements before shifted boundary | +| `Ranged` | Both stamps shifted | Entire range shifted by delay | +| `AnyBefore` | `upper_stamp - delay` | Optional capture before shifted boundary | +| `AnyAtOrBefore` | `upper_stamp - delay` | Optional capture at/before shifted boundary | +| `MatchedStamp` | N/A | No delay (exact match required) | +| `Latched` | Uses `min_period` instead | Different concept for staleness | + +### Quick Decision Guide + +| Situation | Recommended Delay | +|-----------|-------------------| +| Timestamps perfectly aligned | `delay = 0` | +| Follower timestamps late by X | `delay = X` | +| Follower timestamps early by X | `delay = -X` | +| Need historical context | `delay = lookback_time` | +| Unknown timing relationship | Start with `delay = 0`, tune empirically | diff --git a/doc/usage_docs/drivers.md b/doc/usage_docs/drivers.md new file mode 100644 index 00000000..5b91871a --- /dev/null +++ b/doc/usage_docs/drivers.md @@ -0,0 +1,594 @@ +# Driver Execution Examples + +This document provides detailed execution traces for all Flow driver types. Drivers establish the synchronization time range that followers use to select their data. + +--- + +## Table of Contents + +1. [driver::Next](#1-drivernext) +2. [driver::Batch](#2-driverbatch) +3. [driver::Chunk](#3-driverchunk) +4. [driver::Throttled](#4-driverthrottled) +5. [Driver Comparison Summary](#5-driver-comparison-summary) + +--- + +## 1. driver::Next + +**Purpose:** Captures the single oldest element and establishes a point-in-time range. + +**Parameters:** None (default constructor) + +**Range Behavior:** `range.lower_stamp == range.upper_stamp == oldest_element.stamp` + +**Data Removal:** Removes the captured element only + +### Basic Workflow + +```cpp +driver::Next, NoLock> driver; +``` + +#### Example 1.1: Simple Sequential Capture + +``` +INITIAL STATE: + driver.inject(10, "A") + driver.inject(20, "B") + driver.inject(30, "C") + + Queue: [10:"A", 20:"B", 30:"C"] + ↑oldest ↑newest + +CAPTURE 1: + ├─ locate_driver_impl() + │ ├─ queue_.empty()? NO + │ ├─ oldest_stamp() = 10 + │ ├─ range = {lower: 10, upper: 10} + │ └─ RETURN: (PRIMED, ExtractionRange{0, 1}) + │ + ├─ extract_driver_impl() + │ ├─ Move queue_[0] to output: Dispatch{10, "A"} + │ └─ Remove first 1 element + │ + └─ RESULT: State::PRIMED, range={10, 10} + + Output: [Dispatch{10, "A"}] + Queue After: [20:"B", 30:"C"] + ↑oldest ↑newest + +CAPTURE 2: + ├─ locate_driver_impl() + │ ├─ oldest_stamp() = 20 + │ ├─ range = {lower: 20, upper: 20} + │ └─ RETURN: (PRIMED, ExtractionRange{0, 1}) + │ + └─ RESULT: State::PRIMED, range={20, 20} + + Output: [Dispatch{20, "B"}] + Queue After: [30:"C"] + +CAPTURE 3: + └─ RESULT: State::PRIMED, range={30, 30} + + Output: [Dispatch{30, "C"}] + Queue After: [] (empty) + +CAPTURE 4: + ├─ locate_driver_impl() + │ ├─ queue_.empty()? YES + │ └─ RETURN: (RETRY, ExtractionRange{}) + │ + └─ RESULT: State::RETRY (no extraction) +``` + +#### Example 1.2: Out-of-Order Injection + +Flow automatically reorders data by timestamp: + +``` +INJECTION SEQUENCE (out of order): + driver.inject(30, "C") // First injection, but highest stamp + driver.inject(10, "A") // Second injection, lowest stamp + driver.inject(20, "B") // Third injection, middle stamp + + Queue (auto-sorted): [10:"A", 20:"B", 30:"C"] + ↑oldest ↑newest + +CAPTURE 1: + └─ Captures stamp=10 (oldest), NOT stamp=30 (first injected) + + Output: [Dispatch{10, "A"}] +``` + +#### Example 1.3: Rapid Injection During Capture + +``` +TIMELINE: + t=0: Queue = [10, 20, 30] + + CAPTURE starts at t=0: + ├─ locate() returns range={10, 10} + │ + │ >>> During extract(), new data arrives <<< + │ driver.inject(5, "early") // Earlier timestamp! + │ driver.inject(40, "late") // Later timestamp + │ + ├─ extract() completes, removes stamp=10 + └─ RESULT: range={10, 10} + + Queue After: [5, 20, 30, 40] + ↑ New oldest! (will be captured next) + +NEXT CAPTURE: + └─ Captures stamp=5 (not 20!) + + ⚠️ WARNING: This can cause non-monotonic capture if timestamps + go backwards. Use lower_bound parameter to prevent this. +``` + +#### Example 1.4: Using lower_bound Parameter + +```cpp +// Prevent capturing timestamps before a known point +auto result = Synchronizer::capture( + std::forward_as_tuple(driver), + std::forward_as_tuple(output), + 15 // lower_bound: only capture timestamps >= 15 +); +``` + +``` +Queue: [10, 20, 30] +lower_bound = 15 + +CAPTURE: + ├─ locate() returns range={10, 10} + ├─ Check: range.upper_stamp(10) < lower_bound(15)? + │ └─ YES → Set state = ERROR_DRIVER_LOWER_BOUND_EXCEEDED + └─ RESULT: State::ERROR_DRIVER_LOWER_BOUND_EXCEEDED + +⚠️ This is a CRITICAL ERROR indicating timestamp monotonicity violation +``` + +--- + +## 2. driver::Batch + +**Purpose:** Captures N oldest elements, creating a sliding window effect. + +**Parameters:** `size_type batch_size` (must be > 0) + +**Range Behavior:** `range.lower_stamp = oldest.stamp`, `range.upper_stamp = newest_captured.stamp` + +**Data Removal:** Removes ONLY the oldest element (enables overlap) + +### Basic Workflow + +```cpp +driver::Batch, NoLock> driver{3}; // batch_size = 3 +``` + +#### Example 2.1: Sliding Window Effect + +``` +SETUP: + batch_size = 3 + + for (int t = 0; t < 10; t += 2) { + driver.inject(t, t * 0.5); // stamps: 0, 2, 4, 6, 8 + } + + Queue: [0, 2, 4, 6, 8] + +CAPTURE 1: + ├─ locate_driver_impl() + │ ├─ queue_.size() = 5 >= batch_size(3)? YES + │ ├─ range.lower_stamp = queue_[0].stamp = 0 + │ ├─ range.upper_stamp = queue_[2].stamp = 4 + │ └─ RETURN: (PRIMED, ExtractionRange{0, 3}) + │ + ├─ extract_driver_impl() + │ ├─ Move queue_[0..2] to output: stamps 0, 2, 4 + │ └─ Remove first 1 element only (sliding window!) + │ + └─ RESULT: State::PRIMED, range={0, 4} + + Output: [Dispatch{0}, Dispatch{2}, Dispatch{4}] + Queue After: [2, 4, 6, 8] ← stamp 0 removed, others remain + ↑ new oldest + +CAPTURE 2: + ├─ locate_driver_impl() + │ ├─ range = {lower: 2, upper: 6} + │ └─ RETURN: (PRIMED, ExtractionRange{0, 3}) + │ + └─ RESULT: State::PRIMED, range={2, 6} + + Output: [Dispatch{2}, Dispatch{4}, Dispatch{6}] ← OVERLAPPING! + Queue After: [4, 6, 8] + + Notice: Dispatch{2} and Dispatch{4} captured AGAIN (overlap) + +CAPTURE 3: + └─ RESULT: State::PRIMED, range={4, 8} + Output: [Dispatch{4}, Dispatch{6}, Dispatch{8}] + Queue After: [6, 8] ← Only 2 elements remain + +CAPTURE 4: + ├─ locate_driver_impl() + │ ├─ queue_.size() = 2 < batch_size(3)? YES + │ └─ RETURN: (RETRY, ExtractionRange{}) + │ + └─ RESULT: State::RETRY (need 1 more element) +``` + +#### Example 2.2: Batch Size Edge Cases + +``` +CASE A: batch_size = 1 (equivalent to Next) + Queue: [10, 20, 30] + Capture 1: Output=[10], Queue After=[20, 30] + Capture 2: Output=[20], Queue After=[30] + → Behaves exactly like driver::Next + +CASE B: batch_size = queue.size() (full capture) + batch_size = 5 + Queue: [10, 20, 30, 40, 50] + + Capture 1: + Output=[10, 20, 30, 40, 50], range={10, 50} + Queue After=[20, 30, 40, 50] ← Only oldest removed + + Capture 2: + queue.size() = 4 < 5 → RETRY + +CASE C: batch_size = 0 (INVALID) + driver::Batch<...> driver{0}; + → throws std::invalid_argument +``` + +#### Example 2.3: Use Case - Moving Average + +```cpp +// Calculate moving average over 5 samples +driver::Batch, NoLock> driver{5}; + +// Inject sensor readings +for (int t = 0; t < 100; ++t) { + driver.inject(t, read_sensor()); +} + +while (true) { + std::vector> window; + auto result = Synchronizer::capture( + std::forward_as_tuple(driver), + std::forward_as_tuple(std::back_inserter(window)) + ); + + if (result.state == State::PRIMED) { + // window contains 5 overlapping samples + double sum = 0; + for (auto& d : window) sum += d.value; + double avg = sum / 5.0; + + // Next capture will shift window by 1 + } +} +``` + +--- + +## 3. driver::Chunk + +**Purpose:** Captures N oldest elements as a non-overlapping batch. + +**Parameters:** `size_type chunk_size` (must be > 0) + +**Range Behavior:** Same as Batch + +**Data Removal:** Removes ALL captured elements (no overlap) + +### Basic Workflow + +```cpp +driver::Chunk, NoLock> driver{3}; // chunk_size = 3 +``` + +#### Example 3.1: Non-Overlapping Batches + +``` +SETUP: + chunk_size = 3 + Queue: [0, 1, 2, 3, 4, 5, 6, 7, 8] + +CAPTURE 1: + ├─ locate_driver_impl() + │ ├─ queue_.size() = 9 >= chunk_size(3)? YES + │ ├─ range = {lower: 0, upper: 2} + │ └─ RETURN: (PRIMED, ExtractionRange{0, 3}) + │ + ├─ extract_driver_impl() + │ ├─ Move queue_[0..2] to output + │ └─ Remove first 3 elements (ALL captured) + │ + └─ RESULT: State::PRIMED, range={0, 2} + + Output: [0, 1, 2] + Queue After: [3, 4, 5, 6, 7, 8] ← Clean cut! + +CAPTURE 2: + └─ RESULT: State::PRIMED, range={3, 5} + + Output: [3, 4, 5] ← NO OVERLAP with previous + Queue After: [6, 7, 8] + +CAPTURE 3: + └─ RESULT: State::PRIMED, range={6, 8} + + Output: [6, 7, 8] + Queue After: [] + +CAPTURE 4: + └─ RESULT: State::RETRY (empty queue) +``` + +#### Example 3.2: Batch vs Chunk Comparison + +``` +SAME DATA: Queue = [0, 1, 2, 3, 4, 5], size = 3 + +┌─────────────────────────────────────────────────────────┐ +│ driver::Batch{3} │ +├─────────────────────────────────────────────────────────┤ +│ Capture 1: Output=[0,1,2] Queue After=[1,2,3,4,5] │ +│ Capture 2: Output=[1,2,3] Queue After=[2,3,4,5] │ +│ Capture 3: Output=[2,3,4] Queue After=[3,4,5] │ +│ Capture 4: Output=[3,4,5] Queue After=[4,5] │ +│ Capture 5: RETRY (only 2 elements) │ +│ │ +│ Total captures: 4 (with overlap) │ +│ Element 2 captured: 3 times │ +└─────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────┐ +│ driver::Chunk{3} │ +├─────────────────────────────────────────────────────────┤ +│ Capture 1: Output=[0,1,2] Queue After=[3,4,5] │ +│ Capture 2: Output=[3,4,5] Queue After=[] │ +│ Capture 3: RETRY (empty) │ +│ │ +│ Total captures: 2 (no overlap) │ +│ Element 2 captured: 1 time │ +└─────────────────────────────────────────────────────────┘ +``` + +#### Example 3.3: Use Case - Packet Processing + +```cpp +// Process network packets in fixed-size batches +driver::Chunk, NoLock> driver{100}; // 100 packets per batch + +while (receiving) { + driver.inject(sequence_num, packet); +} + +// Process complete batches +while (true) { + std::vector> batch; + auto result = Synchronizer::capture( + std::forward_as_tuple(driver), + std::forward_as_tuple(std::back_inserter(batch)) + ); + + if (result.state == State::PRIMED) { + // Process exactly 100 packets + // No packet processed twice + process_batch(batch); + } else { + break; // Wait for more packets + } +} +``` + +--- + +## 4. driver::Throttled + +**Purpose:** Rate-limited capture that skips elements to maintain a maximum capture rate. + +**Parameters:** `offset_type throttle_period` (minimum time between captures) + +**Range Behavior:** `range.lower_stamp == range.upper_stamp == captured_element.stamp` + +**Data Removal:** Removes captured element AND all older elements + +### Basic Workflow + +```cpp +driver::Throttled, NoLock> driver{10}; // throttle_period = 10 +``` + +#### Example 4.1: Throttling High-Frequency Data + +``` +SETUP: + throttle_period = 10 + + // High-frequency injection (every 2 time units) + for (int t = 0; t <= 30; t += 2) { + driver.inject(t, t * 1.0); // stamps: 0, 2, 4, 6, 8, 10, 12, 14, ... + } + + Queue: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30] + + Internal state: last_captured_stamp_ = min (no previous capture) + +CAPTURE 1: + ├─ locate_driver_impl() + │ ├─ last_captured_stamp_ = min (first capture) + │ ├─ target = min + throttle_period ≈ min + │ ├─ Find first element >= target → stamp 0 + │ ├─ range = {0, 0} + │ └─ RETURN: (PRIMED, ExtractionRange{0, 1}) + │ + ├─ extract_driver_impl() + │ ├─ Move stamp 0 to output + │ ├─ Update last_captured_stamp_ = 0 + │ └─ Remove all elements <= 0 (just stamp 0) + │ + └─ RESULT: State::PRIMED, range={0, 0} + + Output: [Dispatch{0, 0.0}] + Queue After: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30] + +CAPTURE 2: + ├─ locate_driver_impl() + │ ├─ last_captured_stamp_ = 0 + │ ├─ target = 0 + 10 = 10 + │ ├─ Find first element >= 10 → stamp 10 + │ ├─ Skip stamps 2, 4, 6, 8 (too early) + │ ├─ range = {10, 10} + │ └─ RETURN: (PRIMED, ExtractionRange for stamp 10) + │ + ├─ extract_driver_impl() + │ ├─ Move stamp 10 to output + │ ├─ Update last_captured_stamp_ = 10 + │ └─ Remove all elements <= 10 (stamps 2,4,6,8,10) + │ + └─ RESULT: State::PRIMED, range={10, 10} + + Output: [Dispatch{10, 10.0}] + Queue After: [12, 14, 16, 18, 20, 22, 24, 26, 28, 30] + + ⚠️ DROPPED: stamps 2, 4, 6, 8 (within throttle period) + +CAPTURE 3: + ├─ target = 10 + 10 = 20 + ├─ Find first element >= 20 → stamp 20 + ├─ Skip stamps 12, 14, 16, 18 + └─ RESULT: State::PRIMED, range={20, 20} + + Output: [Dispatch{20, 20.0}] + Queue After: [22, 24, 26, 28, 30] + + ⚠️ DROPPED: stamps 12, 14, 16, 18 + +CAPTURE 4: + ├─ target = 20 + 10 = 30 + ├─ Find first element >= 30 → stamp 30 + └─ RESULT: State::PRIMED, range={30, 30} + + Output: [Dispatch{30, 30.0}] + Queue After: [] + + ⚠️ DROPPED: stamps 22, 24, 26, 28 +``` + +#### Example 4.2: Throttle Period > Data Span + +``` +throttle_period = 100 +Queue: [0, 2, 4, 6, 8] + +CAPTURE 1: + ├─ target = min + 100 ≈ min + ├─ Find first >= min → stamp 0 + └─ RESULT: State::PRIMED, range={0, 0} + + Queue After: [2, 4, 6, 8] + +CAPTURE 2: + ├─ target = 0 + 100 = 100 + ├─ Find first >= 100 → NONE FOUND + │ (newest stamp is 8 < 100) + └─ RESULT: State::RETRY + + Queue After: [2, 4, 6, 8] (unchanged) + + → Need to wait for data with stamp >= 100 +``` + +#### Example 4.3: Use Case - Downsampling Camera Stream + +```cpp +// Camera at 60fps, process at 10fps +// throttle_period = 100ms (for 10fps) + +using TimePoint = std::chrono::steady_clock::time_point; +using Duration = std::chrono::milliseconds; +using CameraDispatch = Dispatch; + +driver::Throttled driver{Duration{100}}; + +// Camera callback (60fps) +void on_frame(const cv::Mat& frame) { + driver.inject(std::chrono::steady_clock::now(), frame); +} + +// Processing loop (will run at ~10fps) +while (running) { + std::vector frames; + auto result = Synchronizer::capture(...); + + if (result.state == State::PRIMED) { + // Process ~10 frames per second + // ~5 frames dropped between each capture + process_frame(frames[0].value); + } +} +``` + +--- + +## 5. Driver Comparison Summary + +| Driver | Captures | Removes | Overlap | Use Case | +|--------|----------|---------|---------|----------| +| `Next` | 1 oldest | 1 captured | No | Frame-by-frame | +| `Batch{N}` | N oldest | 1 oldest | Yes | Sliding window | +| `Chunk{N}` | N oldest | N captured | No | Fixed batches | +| `Throttled{P}` | 1 at interval | All before | No (skips) | Rate limiting | + +### Decision Tree + +``` +Need to capture multiple elements per sync? +├─ NO → driver::Next +│ +└─ YES → Need overlap between captures? + ├─ YES → driver::Batch + │ + └─ NO → Need rate limiting? + ├─ YES → driver::Throttled + │ (will drop intermediate data) + │ + └─ NO → driver::Chunk +``` + +### Range Visualization + +``` +Data timeline: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + +driver::Next: + Cap1: [0]───────────────────────────────── range={0,0} + Cap2: [1]────────────────────────────── range={1,1} + Cap3: [2]─────────────────────────── range={2,2} + +driver::Batch{4}: + Cap1: [0, 1, 2, 3]──────────────────────── range={0,3} + Cap2: [1, 2, 3, 4]───────────────────── range={1,4} ← overlap + Cap3: [2, 3, 4, 5]────────────────── range={2,5} ← overlap + +driver::Chunk{4}: + Cap1: [0, 1, 2, 3]──────────────────────── range={0,3} + Cap2: [4, 5, 6, 7]──────────── range={4,7} ← no overlap + Cap3: [8, 9, ...] range={8,...} + +driver::Throttled{5}: + Cap1: [0]───────────────────────────────── range={0,0} + Cap2: [5]────────────────── range={5,5} ← skipped 1-4 + Cap3: [10]─ range={10,10} ← skipped 6-9 +``` diff --git a/doc/usage_docs/edge_cases_and_failures.md b/doc/usage_docs/edge_cases_and_failures.md new file mode 100644 index 00000000..283d8df8 --- /dev/null +++ b/doc/usage_docs/edge_cases_and_failures.md @@ -0,0 +1,697 @@ +# Edge Cases and Failure Scenarios + +This document covers common failure scenarios, edge cases, and their solutions when using Flow. + +--- + +## Table of Contents + +1. [Understanding Capture States](#understanding-capture-states) +2. [ABORT Scenarios](#abort-scenarios) +3. [RETRY Scenarios](#retry-scenarios) +4. [Deadlock and Starvation](#deadlock-and-starvation) +5. [Timestamp Violations](#timestamp-violations) +6. [Configuration Errors](#configuration-errors) +7. [Recovery Strategies](#recovery-strategies) + +--- + +## Understanding Capture States + +Flow uses three states to communicate synchronization status: + +```cpp +enum class State { + PRIMED, // All captors ready, data captured + RETRY, // Need more data, try again later + ABORT // Data gap detected, skip this frame +}; +``` + +### State Propagation Rules + +``` + ┌─────────────────────────────────────────┐ + │ Synchronizer State Logic │ + ├─────────────────────────────────────────┤ + │ │ + │ if (driver == ABORT) → return ABORT │ + │ if (any follower == ABORT) → ABORT │ + │ if (driver == RETRY) → return RETRY │ + │ if (any follower == RETRY) → RETRY │ + │ otherwise → return PRIMED │ + │ │ + │ Priority: ABORT > RETRY > PRIMED │ + │ │ + └─────────────────────────────────────────┘ +``` + +--- + +## ABORT Scenarios + +ABORT indicates irrecoverable data loss - the frame must be skipped. + +### Scenario 1: MatchedStamp Missing Data + +```cpp +driver::Next, NoLock> driver; +follower::MatchedStamp, NoLock> follower; +``` + +``` +DATA STATE: + Driver: [100, 200, 300] + Follower: [100, 300] // Missing stamp 200! + +CAPTURE at t=100: + Driver range: {100, 100} + Follower: find(100) → PRIMED ✓ + +CAPTURE at t=200: + Driver range: {200, 200} + Follower: + ├─ find(200) + ├─ Queue: [300] (100 already consumed) + ├─ oldest(300) > 200 + │ └─ If oldest > target, data will NEVER arrive + │ + └─ State: ABORT + + ⚠️ ABORT: stamp 200 cannot be matched, frame dropped + +CAPTURE at t=300: + Driver range: {300, 300} + Follower: find(300) → PRIMED ✓ + +RESULT: + - t=100: Success + - t=200: DROPPED + - t=300: Success +``` + +### Scenario 2: ClosestBefore No Data in Window + +```cpp +follower::ClosestBefore, NoLock> follower{ + 10, // period + 0 // delay +}; +``` + +``` +DATA STATE: + Driver: [100] + Follower: [80, 120] // Gap around 100! + +CAPTURE at t=100: + Driver range: {100, 100} + Follower: + ├─ boundary = 100 - 0 = 100 + ├─ window = [100 - 10, 100) = [90, 100) + │ + ├─ Elements in [90, 100): NONE + │ 80 < 90, 120 >= 100 + │ + ├─ Have element >= 100? YES (120) + │ → Proves no data [90,100) will arrive + │ + └─ State: ABORT + +FIX: Increase period to cover gap + follower::ClosestBefore<...> follower{30, 0}; // Window [70, 100) + Now stamp 80 is captured! +``` + +### Scenario 3: Before Without Future Proof + +```cpp +follower::Before, NoLock> follower{0}; +``` + +``` +DATA STATE: + Driver: [100, 200] + Follower: [50] // Only one element! + +CAPTURE 1 at t=100: + Driver range: {100, 100} + Follower: + ├─ boundary = 100 + ├─ Elements < 100: [50] ✓ + ├─ Have element >= 100? NO + │ └─ Cannot prove no more data < 100 will arrive + │ + └─ State: RETRY (waiting for proof) + +Follower gets stamp 150: + Follower: [50, 150] + +CAPTURE 1 (retry): + Follower: + ├─ Elements < 100: [50] ✓ + ├─ Have element >= 100? YES (150) + │ └─ Proves 50 is all data < 100 + │ + └─ State: PRIMED + + ⚠️ Note: stamp 150 is the "proof" element, not captured +``` + +### Scenario 4: CountBefore Insufficient Historical Data + +```cpp +follower::CountBefore, NoLock> follower{5, 0}; +``` + +``` +DATA STATE: + Driver: [100] + Follower: [90, 95, 98] // Only 3 elements < 100! + +CAPTURE: + Driver range: {100, 100} + Follower: + ├─ boundary = 100 + ├─ Elements < 100: [90, 95, 98] (count = 3) + ├─ Need: 5 elements + │ + ├─ Have element >= 100? NO + │ → Still might get more < 100 + │ + └─ State: RETRY + +Follower gets stamp 105: + Follower: [90, 95, 98, 105] + +CAPTURE (retry): + Follower: + ├─ Elements < 100: [90, 95, 98] (count = 3) + ├─ Have element >= 100? YES (105) + │ → Proves only 3 elements exist + │ + └─ State: ABORT (will NEVER have 5 elements) + + ⚠️ ABORT: Cannot satisfy count requirement +``` + +--- + +## RETRY Scenarios + +RETRY means more data is needed - synchronization will succeed eventually. + +### Scenario 1: Driver Queue Empty + +```cpp +driver::Next, NoLock> driver; +``` + +``` +CAPTURE when empty: + Driver: + ├─ queue_.empty() == true + └─ State: RETRY + + Synchronizer returns RETRY, waits for data +``` + +### Scenario 2: Ranged Waiting for Upper Bound + +```cpp +follower::Ranged, NoLock> follower{0}; +``` + +``` +DATA STATE: + Driver: [100] + Follower: [80, 90, 100] // No element AFTER 100! + +CAPTURE: + Driver range: {100, 100} + Follower: + ├─ Need: 1 before, elements in [100,100], 1 after + │ + ├─ Before 100: stamps 80, 90 ✓ + ├─ In [100, 100]: stamp 100 ✓ + ├─ After 100: NONE (newest is 100) + │ └─ Cannot determine if more data coming + │ + └─ State: RETRY + +Follower gets stamp 110: + Follower: [80, 90, 100, 110] + +CAPTURE (retry): + Follower: + ├─ After 100: stamp 110 ✓ + └─ State: PRIMED + + Captured: [90, 100, 110] + (80 may be removed, 90 kept as lower bound) +``` + +### Scenario 3: Batch Insufficient Elements + +```cpp +driver::Batch, NoLock> driver{10}; // Need 10 +``` + +``` +CAPTURE: + Driver queue: [0, 10, 20, 30] // Only 4 elements! + + Driver: + ├─ queue_.size() = 4 < min_period (10) + └─ State: RETRY + + Waits until 10+ elements available +``` + +### Scenario 4: Latched No Initial Value + +```cpp +follower::Latched, NoLock> follower{100}; +``` + +``` +DATA STATE (startup): + Driver: [100] + Follower: [] // No config yet! + +CAPTURE: + Driver range: {100, 100} + Follower: + ├─ boundary = 100 - 100 = 0 + ├─ Find stamp <= 0: NONE (queue empty) + ├─ latched_.has_value()? NO (first capture) + │ + │ Two possibilities: + │ 1. Queue empty: RETRY + │ 2. Have newer data but nothing <= boundary: ABORT + │ + └─ State: RETRY (queue empty, might get data) + +Follower gets stamp 50: + Follower: [50] + +CAPTURE (retry): + Driver range: {100, 100} + Follower: + ├─ boundary = 0 + ├─ Find stamp <= 0: NONE + ├─ But have stamp 50 > 0 + │ → Proves nothing <= 0 exists + │ + └─ State: ABORT? + + ⚠️ This depends on implementation details! + + FIX: Ensure config arrives before driver data + OR set min_period appropriately +``` + +--- + +## Deadlock and Starvation + +### Deadlock: Circular Dependency + +``` +SCENARIO: + System A sends data to System B + System B sends data to System A + Both waiting for each other! + + Synchronizer 1: + ├─ Driver: System A output + └─ Follower: System B output (RETRY - waiting) + + Synchronizer 2: + ├─ Driver: System B output + └─ Follower: System A output (RETRY - waiting) + + DEADLOCK: Neither can make progress! + +SOLUTION: + 1. Use separate threads for each synchronizer + 2. Add timeout mechanism + 3. Restructure to break circular dependency +``` + +### Starvation: Slow Follower + +```cpp +driver::Next, NoLock> fast_driver; // 1000Hz +follower::Before, NoLock> slow_follower{0}; // 1Hz +``` + +``` +SCENARIO: + Fast driver: stamps [0, 1, 2, 3, ..., 999] + Slow follower: stamp [0] only + +CAPTURE 1 at t=0: + Driver range: {0, 0} + Follower: + ├─ boundary = 0 + ├─ Elements < 0: NONE + ├─ Have element >= 0? YES (stamp 0) + └─ State: PRIMED (empty capture OK) + +CAPTURE 2 at t=1: + Driver range: {1, 1} + Follower: + ├─ boundary = 1 + ├─ Elements < 1: [0] + ├─ Have element >= 1? NO + │ + └─ State: RETRY + + Fast driver queue grows: [2, 3, 4, ..., 999, 1000, ...] + Synchronizer blocked waiting for slow follower! + +SOLUTION: + 1. Use Throttled driver to match slow rate + 2. Use AnyBefore (won't block on empty) + 3. Buffer management in application layer +``` + +### Starvation: Throttled Skipping All Data + +```cpp +driver::Throttled, NoLock> driver{1000}; // 1Hz output +``` + +``` +DATA STATE: + Data arrives at 10Hz: [0, 100, 200, 300, ...] + +CAPTURE 1: + last_captured_ = MIN + target = MIN + 1000 ≈ MIN + First element >= MIN: stamp 0 + Captured: [0] + last_captured_ = 0 + +CAPTURE 2: + target = 0 + 1000 = 1000 + Data queue: [100, 200, 300, 400, 500, 600, 700, 800, 900] + + Driver: + ├─ Find first >= 1000: NONE (all < 1000) + └─ State: RETRY + + ... time passes, more data arrives ... + + Data queue: [100, 200, ..., 900, 1000, 1100] + +CAPTURE 2 (retry): + Find first >= 1000: stamp 1000 + Captured: [1000] + DROPPED: stamps 100-900 (skipped by throttle) + +RESULT: + Captured: 0, 1000, 2000, ... + Dropped: everything in between + + ⚠️ If data rate < throttle rate: + Most data captured, some RETRY delays + + ⚠️ If data rate > throttle rate: + Significant data dropped (by design!) +``` + +--- + +## Timestamp Violations + +### Non-Monotonic Timestamps + +```cpp +// Flow assumes timestamps are monotonically increasing! +captor.inject(100, data1); +captor.inject(200, data2); +captor.inject(150, data3); // VIOLATION: 150 < 200 +``` + +``` +QUEUE STATE: + After inject(100): [100] + After inject(200): [100, 200] + After inject(150): [100, 200, 150] // Wrong order! + +CAPTURE: + Driver assumes queue is sorted! + + oldest_stamp() may return wrong value + Binary search may fail + Capture ranges may be incorrect + + ⚠️ UNDEFINED BEHAVIOR + +SOLUTION: + 1. Sort data before injection + 2. Use application-level timestamping + 3. Add validation layer: + + void safe_inject(Stamp stamp, Data data) { + if (!queue_.empty() && stamp < queue_.back().stamp()) { + // Handle: discard, reorder, or error + } + captor.inject(stamp, data); + } +``` + +### Timestamp Overflow + +```cpp +using Stamp = int32_t; // Limited range! + +// Near overflow +captor.inject(2147483647, data); // INT32_MAX +captor.inject(2147483647 + 1, data); // Overflow to negative! +``` + +``` +QUEUE STATE: + [2147483647, -2147483648] // Apparent time reversal! + +SOLUTION: + 1. Use int64_t or uint64_t timestamps + 2. Use relative timestamps with epoch reset + 3. Implement wrap-around handling +``` + +### Very Large Timestamp Gaps + +```cpp +driver::Throttled, NoLock> driver{1000}; +``` + +``` +DATA STATE: + Normal: [0, 1, 2, ..., 999] + Then gap: [0, 1, ..., 999, 1000000] // Jump of ~1 second to ~17 minutes + +CAPTURE after gap: + last_captured_ = 999 + target = 999 + 1000 = 1999 + + Find first >= 1999: stamp 1000000 + + ├─ Captures stamp 1000000 + └─ last_captured_ = 1000000 + +NEXT CAPTURE: + target = 1000000 + 1000 = 1001000 + + ⚠️ If data resumes at normal rate: + stamps [1001, 1002, ...] all < 1001000 + All SKIPPED until stamp >= 1001000! + +SOLUTION: + 1. Detect and handle gaps in application + 2. Reset synchronizer on large gaps + 3. Use adaptive throttle period +``` + +--- + +## Configuration Errors + +### Period Too Small + +```cpp +follower::ClosestBefore, NoLock> follower{1, 0}; +// Period = 1, but data arrives every 10ms +``` + +``` +DATA STATE: + Data: [0, 10, 20, 30, ...] // 10ms intervals + +CAPTURE at t=100: + Driver range: {100, 100} + Follower: + ├─ boundary = 100, window = [99, 100) + ├─ No data in [99, 100)! + │ (stamp 100 is >= 100, not in window) + │ + └─ State: ABORT + +EVERY capture fails! + +FIX: period >= data interval + follower::ClosestBefore<...> follower{15, 0}; // Window [85, 100) + Now captures stamp 90 or 100 +``` + +### Delay Misconfiguration + +```cpp +follower::Before, NoLock> follower{100}; +// Delay = 100, expects data 100ms before driver +``` + +``` +DATA STATE: + Driver: [100, 200, 300] + Follower: [100, 200, 300] // Same timestamps as driver! + +CAPTURE at t=100: + Driver range: {100, 100} + Follower: + ├─ boundary = 100 - 100 = 0 + ├─ Elements < 0: NONE + │ + └─ No data captured (all follower stamps > 0) + + ⚠️ Delay doesn't match actual data relationship! + +FIX: Match delay to actual timing relationship + follower::Before<...> follower{0}; // Same timestamps +``` + +### Count Larger Than Available + +```cpp +follower::CountBefore, NoLock> follower{100, 0}; +// Requires 100 historical elements +``` + +``` +DATA STATE: + Stream only produces 10 elements total! + +RESULT: + Every capture attempt → ABORT + System never makes progress + +FIX: Set count to realistic value + follower::CountBefore<...> follower{5, 0}; +``` + +--- + +## Recovery Strategies + +### Strategy 1: Retry with Timeout + +```cpp +template +State capture_with_timeout(Sync& sync, + std::chrono::milliseconds timeout) { + auto start = std::chrono::steady_clock::now(); + + while (true) { + State state = sync.capture(...); + + if (state != State::RETRY) { + return state; + } + + auto elapsed = std::chrono::steady_clock::now() - start; + if (elapsed > timeout) { + return State::ABORT; // Give up + } + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} +``` + +### Strategy 2: Skip ABORTs and Continue + +```cpp +while (true) { + State state = sync.capture(...); + + switch (state) { + case State::PRIMED: + process_data(...); + break; + + case State::RETRY: + // Wait for more data + wait_for_data(); + break; + + case State::ABORT: + // Log and continue + log_dropped_frame(); + // Data already removed, next capture will try new data + break; + } +} +``` + +### Strategy 3: Fallback to Partial Data + +```cpp +// Use AnyBefore/AnyAtOrBefore for optional data +follower::AnyBefore, NoLock> optional{0}; + +// These never ABORT - empty result is valid +// Application handles missing data gracefully +``` + +### Strategy 4: Dynamic Reconfiguration + +```cpp +// Monitor ABORT rate +int abort_count = 0; +int total_count = 0; + +while (true) { + State state = sync.capture(...); + total_count++; + + if (state == State::ABORT) { + abort_count++; + + float abort_rate = float(abort_count) / total_count; + if (abort_rate > 0.5) { // >50% failures + // Reconfigure: increase periods, reduce requirements + reconfigure_captors(); + abort_count = 0; + total_count = 0; + } + } +} +``` + +--- + +## Summary: Common Pitfalls + +| Issue | Symptom | Solution | +|-------|---------|----------| +| MatchedStamp missing data | ABORT | Use ClosestBefore or AnyBefore | +| Period too small | Frequent ABORT | Increase period to match data rate | +| Count too large | Permanent ABORT | Reduce count or use Before | +| Delay mismatch | Empty captures | Match delay to actual timing | +| Slow follower | RETRY forever | Use Throttled driver or AnyBefore | +| Non-monotonic timestamps | Undefined behavior | Sort before inject | +| Empty queues at start | RETRY | Pre-populate or handle startup | +| Large timestamp gaps | Skipped data | Detect gaps, reset state | diff --git a/doc/usage_docs/followers.md b/doc/usage_docs/followers.md new file mode 100644 index 00000000..35ec6156 --- /dev/null +++ b/doc/usage_docs/followers.md @@ -0,0 +1,939 @@ +# Follower Execution Examples + +This document provides detailed execution traces for all Flow follower types. Followers select data based on the synchronization range established by a driver. + +--- + +## Table of Contents + +1. [follower::Before](#1-followerbefore) +2. [follower::ClosestBefore](#2-followerclosestbefore) +3. [follower::CountBefore](#3-followercountbefore) +4. [follower::Ranged](#4-followerranged) +5. [follower::MatchedStamp](#5-followermatchedstamp) +6. [follower::Latched](#6-followerlatched) +7. [follower::AnyBefore](#7-followeranybefore) +8. [follower::AnyAtOrBefore](#8-followeranyatorbefore) +9. [Follower Comparison Summary](#9-follower-comparison-summary) + +--- + +## 1. follower::Before + +**Purpose:** Captures ALL elements before the synchronization boundary. + +**Parameters:** `offset_type delay` (offset from driver's range) + +**Boundary:** `range.upper_stamp - delay` (non-inclusive) + +> ⚠️ **Note:** The README.md states this uses `lower_stamp`, but the actual implementation uses `upper_stamp`. For `driver::Next` they are equal, but for `Batch`/`Chunk` this matters. See [Boundary Calculation Reference](./boundary_calculation.md). + +**Requirements:** At least one element AFTER boundary must exist (proves data is complete) + +**Data Removal:** All captured elements removed + +### Basic Workflow + +```cpp +follower::Before, NoLock> follower{5}; // delay = 5 +``` + +#### Example 1.1: Standard Capture + +``` +SETUP: + delay = 5 + Driver establishes range = {20, 20} + + Follower queue: [5, 8, 10, 12, 15, 18, 22, 25] + ↑oldest ↑newest + +CAPTURE: + ├─ locate_follower_impl(range={20, 20}) + │ ├─ boundary = range.upper_stamp - delay = 20 - 5 = 15 + │ │ + │ ├─ Check: queue_.empty()? NO + │ ├─ Check: newest_stamp(25) >= boundary(15)? YES + │ │ (proves we have data after boundary) + │ │ + │ ├─ Iterate: collect stamps < 15 + │ │ ├─ stamp 5 < 15 → INCLUDE + │ │ ├─ stamp 8 < 15 → INCLUDE + │ │ ├─ stamp 10 < 15 → INCLUDE + │ │ ├─ stamp 12 < 15 → INCLUDE + │ │ └─ stamp 15 >= 15 → STOP + │ │ + │ └─ RETURN: (PRIMED, ExtractionRange{0, 4}) + │ + ├─ extract_follower_impl() + │ ├─ Move elements [0..3] to output: stamps 5, 8, 10, 12 + │ └─ Remove first 4 elements + │ + └─ RESULT: State::PRIMED + + Output: [Dispatch{5}, Dispatch{8}, Dispatch{10}, Dispatch{12}] + Queue After: [15, 18, 22, 25] +``` + +#### Example 1.2: RETRY - No Data After Boundary + +``` +SETUP: + delay = 5 + Driver range = {20, 20} + boundary = 15 + + Follower queue: [5, 8, 10, 12] ← All before boundary, nothing after! + +CAPTURE: + ├─ locate_follower_impl(range={20, 20}) + │ ├─ boundary = 15 + │ ├─ newest_stamp(12) >= boundary(15)? NO! + │ │ └─ Cannot prove data before boundary is complete + │ │ + │ └─ RETURN: (RETRY, ExtractionRange{}) + │ + └─ RESULT: State::RETRY (no extraction) + + Queue After: [5, 8, 10, 12] (unchanged) + + → Need data with stamp >= 15 to prove completeness +``` + +#### Example 1.3: Empty Capture (All Data After Boundary) + +``` +SETUP: + delay = 5 + Driver range = {20, 20} + boundary = 15 + + Follower queue: [16, 18, 22, 25] ← All >= 15! + +CAPTURE: + ├─ locate_follower_impl() + │ ├─ boundary = 15 + │ ├─ newest_stamp(25) >= 15? YES (have proof) + │ ├─ Iterate: collect stamps < 15 + │ │ └─ stamp 16 >= 15 → STOP (nothing to capture) + │ │ + │ └─ RETURN: (PRIMED, ExtractionRange{0, 0}) ← Empty range! + │ + └─ RESULT: State::PRIMED (with empty output) + + Output: [] (empty, but valid!) + Queue After: [16, 18, 22, 25] (unchanged) +``` + +#### Example 1.4: Large Delay Effect + +``` +SETUP: + delay = 100 ← Large delay + Driver range = {50, 50} + boundary = 50 - 100 = -50 ← Negative boundary! + + Follower queue: [10, 20, 30, 40, 60] + +CAPTURE: + ├─ boundary = -50 + ├─ Iterate: collect stamps < -50 + │ └─ stamp 10 >= -50 → STOP (nothing before -50) + │ + └─ RESULT: State::PRIMED (empty output) + + Output: [] (no data before t=-50) + + ⚠️ With large delays, you may capture nothing! +``` + +--- + +## 2. follower::ClosestBefore + +**Purpose:** Captures the SINGLE element closest to (but before) the boundary. + +**Parameters:** +- `offset_type period` - Expected data period (search window size) +- `offset_type delay` - Offset from driver's range + +**Boundary:** `range.lower_stamp - delay` + +> ✅ This follower correctly uses `lower_stamp` as documented. + +**Search Window:** `[boundary - period, boundary)` + +**Data Removal:** All elements before the captured one (inclusive) + +### Basic Workflow + +```cpp +follower::ClosestBefore, NoLock> follower{ + 10, // period: expect data every ~10 time units + 5 // delay: look 5 time units before driver +}; +``` + +#### Example 2.1: Standard Capture + +``` +SETUP: + period = 10, delay = 5 + Driver range = {100, 100} + + boundary = 100 - 5 = 95 + search_window = [95 - 10, 95) = [85, 95) + + Follower queue: [70, 80, 88, 92, 98, 105] + +CAPTURE: + ├─ locate_follower_impl(range={100, 100}) + │ ├─ boundary = 95 + │ ├─ search_window = [85, 95) + │ │ + │ ├─ Check: Have element >= boundary? + │ │ └─ stamp 98 >= 95? YES (proves search is complete) + │ │ + │ ├─ Find closest in window [85, 95): + │ │ ├─ stamp 70: not in [85, 95) + │ │ ├─ stamp 80: not in [85, 95) + │ │ ├─ stamp 88: IN [85, 95) ✓ + │ │ ├─ stamp 92: IN [85, 95) ✓ ← CLOSEST to 95 + │ │ └─ stamp 98: not in [85, 95) + │ │ + │ └─ RETURN: (PRIMED, ExtractionRange pointing to stamp 92) + │ + ├─ extract_follower_impl() + │ ├─ Move stamp 92 to output + │ └─ Remove stamps 70, 80, 88, 92 (all up to captured) + │ + └─ RESULT: State::PRIMED + + Output: [Dispatch{92, ...}] + Queue After: [98, 105] +``` + +#### Example 2.2: ABORT - No Element in Window + +``` +SETUP: + period = 10, delay = 5 + Driver range = {100, 100} + search_window = [85, 95) + + Follower queue: [70, 80, 98, 105] ← Gap! No data in [85, 95) + +CAPTURE: + ├─ locate_follower_impl() + │ ├─ boundary = 95, window = [85, 95) + │ ├─ Have element >= 95? YES (stamp 98) + │ ├─ Find closest in [85, 95): + │ │ ├─ stamp 70: not in window + │ │ ├─ stamp 80: not in window (80 < 85) + │ │ ├─ stamp 98: not in window (98 >= 95) + │ │ └─ NO ELEMENT FOUND IN WINDOW + │ │ + │ └─ RETURN: (ABORT, ExtractionRange{}) + │ + └─ RESULT: State::ABORT + + ⚠️ ABORT indicates the data gap is permanent - no point waiting + The synchronization frame is skipped/dropped +``` + +#### Example 2.3: RETRY - No Proof of Completeness + +``` +SETUP: + period = 10, delay = 5 + Driver range = {100, 100} + search_window = [85, 95) + + Follower queue: [70, 80, 88, 92] ← No data >= 95! + +CAPTURE: + ├─ locate_follower_impl() + │ ├─ boundary = 95, window = [85, 95) + │ ├─ Have element >= 95? NO (newest is 92) + │ │ └─ Cannot confirm 92 is the closest + │ │ (data with stamp 94 might arrive!) + │ │ + │ └─ RETURN: (RETRY, ExtractionRange{}) + │ + └─ RESULT: State::RETRY + + Queue After: [70, 80, 88, 92] (unchanged) + + → Need to wait for data with stamp >= 95 +``` + +#### Example 2.4: Period Misconfiguration Warning + +``` +SETUP: + period = 5 ← TOO SMALL! Actual data period is ~20 + delay = 0 + Driver range = {100, 100} + search_window = [95, 100) ← Very narrow! + + Follower queue: [60, 80, 105] ← Data every ~20-25 units + +CAPTURE: + ├─ Find in [95, 100): + │ ├─ stamp 60: not in window + │ ├─ stamp 80: not in window (< 95) + │ └─ stamp 105: not in window (>= 100) + │ + └─ RESULT: State::ABORT (missed stamp 80!) + + ⚠️ WARNING: Period too small causes unnecessary ABORTs! + Solution: Set period >= actual_data_period +``` + +--- + +## 3. follower::CountBefore + +**Purpose:** Captures exactly N elements before the boundary. + +**Parameters:** +- `size_type count` - Number of elements to capture +- `offset_type delay` - Offset from driver's range + +**Boundary:** `range.upper_stamp - delay` + +> ⚠️ **Note:** The README.md states this uses `lower_stamp`, but the actual implementation uses `upper_stamp`. See [Boundary Calculation Reference](./boundary_calculation.md). + +**Data Removal:** All elements before the N-th captured element + +### Basic Workflow + +```cpp +follower::CountBefore, NoLock> follower{ + 3, // count: capture exactly 3 elements + 5 // delay: look 5 time units before driver +}; +``` + +#### Example 3.1: Standard Capture + +``` +SETUP: + count = 3, delay = 5 + Driver range = {100, 100} + boundary = 95 + + Follower queue: [70, 80, 85, 90, 98, 105] + +CAPTURE: + ├─ locate_follower_impl(range={100, 100}) + │ ├─ boundary = 95 + │ │ + │ ├─ Count elements < 95: + │ │ stamps 70, 80, 85, 90 → 4 elements < 95 + │ │ + │ ├─ Have at least count(3) elements? YES + │ ├─ Have element >= 95 (proof)? YES (stamp 98) + │ │ + │ ├─ Select last 3 before boundary: 80, 85, 90 + │ │ (not 70 - it's the 4th oldest) + │ │ + │ └─ RETURN: (PRIMED, ExtractionRange for 80, 85, 90) + │ + ├─ extract_follower_impl() + │ ├─ Move stamps 80, 85, 90 to output + │ └─ Remove stamps 70, 80, 85, 90 (including older) + │ + └─ RESULT: State::PRIMED + + Output: [Dispatch{80}, Dispatch{85}, Dispatch{90}] + Queue After: [98, 105] +``` + +#### Example 3.2: RETRY - Not Enough Elements + +``` +SETUP: + count = 5, delay = 5 + Driver range = {100, 100} + boundary = 95 + + Follower queue: [80, 85, 90, 98] ← Only 3 elements < 95! + +CAPTURE: + ├─ locate_follower_impl() + │ ├─ boundary = 95 + │ ├─ Elements < 95: stamps 80, 85, 90 (only 3) + │ ├─ Have count(5) elements? NO (only 3) + │ │ + │ └─ RETURN: (RETRY, ExtractionRange{}) + │ + └─ RESULT: State::RETRY + + → Need 2 more elements with stamp < 95 + + ⚠️ This might wait forever if no more old data arrives! +``` + +#### Example 3.3: ABORT - Data Gap + +``` +SETUP: + count = 3, delay = 5 + Driver range = {100, 100} + boundary = 95 + + Follower queue: [98, 105, 110] ← All elements >= 95! + +CAPTURE: + ├─ locate_follower_impl() + │ ├─ boundary = 95 + │ ├─ Elements < 95: NONE + │ ├─ Have element >= 95? YES + │ │ └─ Proof that no old data will arrive + │ │ + │ └─ RETURN: (ABORT, ExtractionRange{}) + │ + └─ RESULT: State::ABORT + + ⚠️ Cannot fulfill count requirement - frame dropped +``` + +--- + +## 4. follower::Ranged + +**Purpose:** Captures elements spanning the ENTIRE driver range, plus one before and one after. + +**Parameters:** `offset_type delay` - Offset from driver's range + +**Capture:** Elements from `(range.lower_stamp - delay)` to `(range.upper_stamp - delay)` inclusive, plus boundaries + +**Data Removal:** All elements up to and including the "before" boundary element + +### Basic Workflow + +```cpp +follower::Ranged, NoLock> follower{0}; // delay = 0 +``` + +#### Example 4.1: Standard Capture with Range Span + +``` +SETUP: + delay = 0 + Driver range = {100, 120} ← Range spans 100 to 120 + + Follower queue: [80, 90, 95, 105, 110, 115, 125, 130] + +CAPTURE: + ├─ locate_follower_impl(range={100, 120}) + │ ├─ Need: + │ │ ├─ 1 element BEFORE 100 (boundary anchor) + │ │ ├─ All elements IN [100, 120] + │ │ └─ 1 element AFTER 120 (boundary anchor) + │ │ + │ ├─ Find before 100: stamp 95 (closest < 100) + │ ├─ Find after 120: stamp 125 (closest > 120) + │ ├─ Elements in range: 105, 110, 115 + │ │ + │ ├─ Capture set: [95, 105, 110, 115, 125] + │ │ (before, in-range, after) + │ │ + │ └─ RETURN: (PRIMED, ExtractionRange) + │ + ├─ extract_follower_impl() + │ ├─ Move [95, 105, 110, 115, 125] to output + │ └─ Remove [80, 90, 95] (up to and including "before") + │ + └─ RESULT: State::PRIMED + + Output: [Dispatch{95}, Dispatch{105}, Dispatch{110}, + Dispatch{115}, Dispatch{125}] + Queue After: [105, 110, 115, 125, 130] + + Note: 105, 110, 115, 125 remain (might be needed for next capture) +``` + +#### Example 4.2: Point Range (lower == upper) + +``` +SETUP: + delay = 0 + Driver range = {100, 100} ← Point in time (from driver::Next) + + Follower queue: [80, 90, 95, 100, 105, 110] + +CAPTURE: + ├─ Need: + │ ├─ 1 before 100: stamp 95 + │ ├─ Elements in [100, 100]: stamp 100 (if exists) + │ └─ 1 after 100: stamp 105 + │ + ├─ Capture: [95, 100, 105] + │ (If stamp 100 doesn't exist, still captures [95, 105]) + │ + └─ RESULT: State::PRIMED + + Output: [Dispatch{95}, Dispatch{100}, Dispatch{105}] +``` + +#### Example 4.3: ABORT - No Element Before + +``` +SETUP: + delay = 0 + Driver range = {100, 120} + + Follower queue: [105, 110, 115, 125] ← Nothing < 100! + +CAPTURE: + ├─ Find before 100: NOT FOUND + │ └─ Cannot anchor the range start + │ + └─ RESULT: State::ABORT + + ⚠️ ABORT because we can't guarantee data before the range +``` + +#### Example 4.4: RETRY - No Element After + +``` +SETUP: + delay = 0 + Driver range = {100, 120} + + Follower queue: [80, 90, 95, 105, 110, 115] ← Nothing > 120! + +CAPTURE: + ├─ Find before 100: stamp 95 ✓ + ├─ Find after 120: NOT FOUND + │ └─ Cannot confirm range end is complete + │ + └─ RESULT: State::RETRY + + → Need data with stamp > 120 +``` + +--- + +## 5. follower::MatchedStamp + +**Purpose:** Captures element with EXACT timestamp match to driver's range. + +**Parameters:** None (default constructor) + +**Requirement:** `element.stamp == range.lower_stamp` + +**Data Removal:** All elements up to and including matched element + +### Basic Workflow + +```cpp +follower::MatchedStamp, NoLock> follower; +``` + +#### Example 5.1: Exact Match Found + +``` +SETUP: + Driver range = {100, 100} + + Follower queue: [80, 90, 100, 110, 120] + ↑ Exact match! + +CAPTURE: + ├─ locate_follower_impl(range={100, 100}) + │ ├─ Search for stamp == 100 + │ ├─ Found: stamp 100 exists! + │ │ + │ └─ RETURN: (PRIMED, ExtractionRange for stamp 100) + │ + ├─ extract_follower_impl() + │ ├─ Move stamp 100 to output + │ └─ Remove stamps 80, 90, 100 + │ + └─ RESULT: State::PRIMED + + Output: [Dispatch{100, ...}] + Queue After: [110, 120] +``` + +#### Example 5.2: ABORT - Stamp Missed (Gap) + +``` +SETUP: + Driver range = {100, 100} + + Follower queue: [80, 90, 105, 110] ← No stamp 100! + ↑ Jumped past 100 + +CAPTURE: + ├─ locate_follower_impl(range={100, 100}) + │ ├─ Search for stamp == 100 + │ ├─ Found stamp > 100 (stamp 105) + │ │ └─ Proves stamp 100 will never arrive + │ │ + │ └─ RETURN: (ABORT, ExtractionRange{}) + │ + └─ RESULT: State::ABORT + + Queue After: [80, 90, 105, 110] (unchanged until abort cleanup) + + ⚠️ ABORT: exact match impossible, frame dropped +``` + +#### Example 5.3: RETRY - Not Yet Available + +``` +SETUP: + Driver range = {100, 100} + + Follower queue: [80, 90, 95] ← All < 100 + +CAPTURE: + ├─ locate_follower_impl(range={100, 100}) + │ ├─ Search for stamp == 100 + │ ├─ newest_stamp(95) < 100 + │ │ └─ Stamp 100 might still arrive + │ │ + │ └─ RETURN: (RETRY, ExtractionRange{}) + │ + └─ RESULT: State::RETRY + + → Wait for stamp 100 to arrive +``` + +--- + +## 6. follower::Latched + +**Purpose:** Captures and HOLDS the last valid element, returning it on subsequent captures. + +**Parameters:** `offset_type min_period` - Minimum time between data updates + +**Behavior:** +- First capture: waits for data +- Subsequent: returns latched value OR updates if newer data available + +**Data Removal:** All elements before latched element + +### Basic Workflow + +```cpp +follower::Latched, NoLock> follower{50}; // min_period = 50 +``` + +#### Example 6.1: Initial Latch and Retention + +``` +SETUP: + min_period = 50 + + // Sparse data injection + follower.inject(0, "config_v1"); + follower.inject(100, "config_v2"); + follower.inject(200, "config_v3"); + + Follower queue: [0:"v1", 100:"v2", 200:"v3"] + latched_ = nullopt (nothing latched yet) + +CAPTURE 1 with Driver range = {60, 60}: + ├─ locate_follower_impl(range={60, 60}) + │ ├─ boundary = 60 - 50 = 10 + │ ├─ Find element with stamp <= 10 + │ │ └─ stamp 0 <= 10 ✓ + │ │ + │ └─ RETURN: (PRIMED, ExtractionRange for stamp 0) + │ + ├─ extract_follower_impl() + │ ├─ Move stamp 0 to output + │ ├─ Set latched_ = Dispatch{0, "config_v1"} + │ └─ Remove stamp 0 + │ + └─ RESULT: State::PRIMED + + Output: [Dispatch{0, "config_v1"}] + latched_ = Dispatch{0, "config_v1"} + Queue After: [100:"v2", 200:"v3"] + +CAPTURE 2 with Driver range = {80, 80}: + ├─ locate_follower_impl(range={80, 80}) + │ ├─ boundary = 80 - 50 = 30 + │ ├─ Find element with stamp <= 30 + │ │ └─ NONE in queue (oldest is 100) + │ ├─ But latched_ has value! + │ │ + │ └─ RETURN: (PRIMED, ExtractionRange{0, 0}) ← Use latched! + │ + ├─ extract_follower_impl() + │ └─ Copy latched_ to output (no queue modification) + │ + └─ RESULT: State::PRIMED + + Output: [Dispatch{0, "config_v1"}] ← Same as before! + Queue After: [100:"v2", 200:"v3"] (unchanged) + +CAPTURE 3 with Driver range = {160, 160}: + ├─ locate_follower_impl(range={160, 160}) + │ ├─ boundary = 160 - 50 = 110 + │ ├─ Find element with stamp <= 110 + │ │ └─ stamp 100 <= 110 ✓ (NEW DATA!) + │ │ + │ └─ RETURN: (PRIMED, ExtractionRange for stamp 100) + │ + ├─ extract_follower_impl() + │ ├─ Move stamp 100 to output + │ ├─ Update latched_ = Dispatch{100, "config_v2"} + │ └─ Remove stamp 100 + │ + └─ RESULT: State::PRIMED + + Output: [Dispatch{100, "config_v2"}] ← Updated! + latched_ = Dispatch{100, "config_v2"} + Queue After: [200:"v3"] +``` + +#### Example 6.2: RETRY - No Initial Data + +``` +SETUP: + min_period = 50 + latched_ = nullopt + Follower queue: [] ← Empty! + +CAPTURE with Driver range = {100, 100}: + ├─ locate_follower_impl() + │ ├─ queue_.empty()? YES + │ ├─ latched_.has_value()? NO + │ │ + │ └─ RETURN: (RETRY, ExtractionRange{}) + │ + └─ RESULT: State::RETRY + + ⚠️ Latched needs at least one initial value! +``` + +#### Example 6.3: ABORT - All Data Too New + +``` +SETUP: + min_period = 50 + latched_ = nullopt + Follower queue: [200:"v3", 300:"v4"] ← All stamps far in future + +CAPTURE with Driver range = {100, 100}: + ├─ boundary = 100 - 50 = 50 + ├─ Find stamp <= 50: NONE (oldest is 200) + ├─ latched_.has_value()? NO + ├─ Have element > boundary? YES (200 > 50) + │ └─ Proves no older data will arrive + │ + └─ RESULT: State::ABORT +``` + +#### Example 6.4: Reset Clears Latch + +``` +CAPTURE SEQUENCE: + 1. Capture → latched_ = Dispatch{100, "v2"} + 2. Capture → uses latched (no new data) + 3. follower.reset() ← RESET CALLED + └─ latched_ = nullopt + 4. Capture → RETRY (no latched value!) +``` + +--- + +## 7. follower::AnyBefore + +**Purpose:** Optionally captures ALL elements before boundary. ALWAYS returns PRIMED. + +**Parameters:** `offset_type delay` - Offset from driver's range + +**Boundary:** `range.upper_stamp - delay` + +> ⚠️ **Note:** The README.md states this uses `lower_stamp`, but the actual implementation uses `upper_stamp`. See [Boundary Calculation Reference](./boundary_calculation.md). + +**Behavior:** Captures whatever is available, including nothing + +**Data Removal:** All elements before boundary + +### Basic Workflow + +```cpp +follower::AnyBefore, NoLock> follower{5}; // delay = 5 +``` + +#### Example 7.1: Capture Available Data + +``` +SETUP: + delay = 5 + Driver range = {100, 100} + boundary = 95 + + Follower queue: [80, 85, 90, 98, 105] + +CAPTURE: + ├─ locate_follower_impl(range={100, 100}) + │ ├─ boundary = 95 + │ ├─ Collect all stamps < 95: [80, 85, 90] + │ │ + │ └─ RETURN: (PRIMED, ExtractionRange{0, 3}) ← Always PRIMED! + │ + └─ RESULT: State::PRIMED + + Output: [Dispatch{80}, Dispatch{85}, Dispatch{90}] + Queue After: [98, 105] +``` + +#### Example 7.2: Empty Queue - Still PRIMED + +``` +SETUP: + delay = 5 + Driver range = {100, 100} + + Follower queue: [] ← Empty! + +CAPTURE: + ├─ locate_follower_impl() + │ ├─ boundary = 95 + │ ├─ queue_.empty()? YES + │ │ + │ └─ RETURN: (PRIMED, ExtractionRange{0, 0}) ← Still PRIMED! + │ + └─ RESULT: State::PRIMED + + Output: [] ← Empty but valid + + ✓ Perfect for OPTIONAL data streams! +``` + +#### Example 7.3: All Data After Boundary - Still PRIMED + +``` +SETUP: + delay = 5 + Driver range = {100, 100} + boundary = 95 + + Follower queue: [98, 105, 110] ← All >= 95 + +CAPTURE: + ├─ Collect stamps < 95: NONE + │ + └─ RESULT: State::PRIMED + + Output: [] ← Empty but valid + Queue After: [98, 105, 110] (unchanged) +``` + +#### Example 7.4: Non-Determinism Warning + +``` +⚠️ WARNING: AnyBefore can behave non-deterministically! + +SCENARIO A - Fast data arrival: + t=0: Driver range = {100, 100} + t=0: Follower queue = [80, 85, 90] + t=0: Capture → Output: [80, 85, 90] + +SCENARIO B - Slow data arrival (same logical time): + t=0: Driver range = {100, 100} + t=0: Follower queue = [80, 85] ← 90 not arrived yet! + t=0: Capture → Output: [80, 85] ← DIFFERENT RESULT! + + Later: stamp 90 arrives but boundary already passed + → stamp 90 will be captured in NEXT sync frame + +SOLUTION: Use appropriate delay to allow data to arrive + Or use follower::Before for deterministic behavior +``` + +--- + +## 8. follower::AnyAtOrBefore + +**Purpose:** Same as AnyBefore but INCLUDES elements AT the boundary. + +**Parameters:** `offset_type delay` - Offset from driver's range + +**Boundary:** `range.upper_stamp - delay` (INCLUSIVE) + +> ⚠️ **Note:** The README.md states this uses `lower_stamp`, but the actual implementation uses `upper_stamp`. See [Boundary Calculation Reference](./boundary_calculation.md). + +**Behavior:** Captures stamps <= boundary (not just <) + +### Example 8.1: Inclusive Boundary + +``` +SETUP: + delay = 5 + Driver range = {100, 100} + boundary = 95 + + Follower queue: [80, 85, 90, 95, 98, 105] + ↑ AT boundary + +CAPTURE with AnyBefore (exclusive): + └─ Collects stamps < 95: [80, 85, 90] + Output: [80, 85, 90] ← stamp 95 NOT included + +CAPTURE with AnyAtOrBefore (inclusive): + └─ Collects stamps <= 95: [80, 85, 90, 95] + Output: [80, 85, 90, 95] ← stamp 95 INCLUDED +``` + +--- + +## 9. Follower Comparison Summary + +| Follower | Captures | Requires Proof? | Can be Empty? | Use Case | +|----------|----------|-----------------|---------------|----------| +| `Before` | All < boundary | Yes (element after) | Yes | Historical context | +| `ClosestBefore` | 1 in window | Yes | No | Nearest-neighbor | +| `CountBefore` | Exactly N | Yes | No | Fixed history | +| `Ranged` | Range + boundaries | Yes (both ends) | No* | Interpolation | +| `MatchedStamp` | Exact match | N/A | No | Synchronized streams | +| `Latched` | 1, then holds | For first | No (after first) | Slow-updating state | +| `AnyBefore` | All < boundary | No (ALWAYS PRIMED) | Yes | Optional streams | +| `AnyAtOrBefore` | All <= boundary | No (ALWAYS PRIMED) | Yes | Optional + boundary | + +*Ranged can capture no "in-range" elements if range is a point + +### State Outcome Comparison + +``` +Given: Driver range = {100, 100} + +follower::Before{5}: boundary = 95 (exclusive) +follower::ClosestBefore{10, 5}: window = [85, 95) +follower::CountBefore{3, 5}: need 3 elements < 95 +follower::MatchedStamp: need stamp == 100 +follower::Latched{50}: boundary = 50 + +Queue A: [80, 90, 102] +├─ Before: PRIMED (captures 80, 90) +├─ ClosestBefore: PRIMED (captures 90) +├─ CountBefore: RETRY (only 2 elements < 95) +├─ MatchedStamp: ABORT (jumped past 100) +└─ Latched: PRIMED (captures 80) + +Queue B: [80, 90] +├─ Before: RETRY (no proof, need element >= 95) +├─ ClosestBefore: RETRY (no proof) +├─ CountBefore: RETRY (no proof) +├─ MatchedStamp: RETRY (stamp 100 might arrive) +└─ Latched: PRIMED (captures 80) + +Queue C: [] +├─ Before: RETRY (empty) +├─ ClosestBefore: RETRY (empty) +├─ CountBefore: RETRY (empty) +├─ MatchedStamp: RETRY (empty) +├─ Latched: RETRY (no latched value) +├─ AnyBefore: PRIMED (empty is OK!) ← Only optional follower +└─ AnyAtOrBefore: PRIMED (empty is OK!) +``` diff --git a/doc/usage_docs/message_dropping.md b/doc/usage_docs/message_dropping.md new file mode 100644 index 00000000..707b1d0e --- /dev/null +++ b/doc/usage_docs/message_dropping.md @@ -0,0 +1,606 @@ +# Message Dropping Behavior + +This document details when and why messages are dropped in Flow, including intentional throttling, boundary cleanup, and edge cases. + +--- + +## Table of Contents + +1. [Categories of Message Dropping](#categories-of-message-dropping) +2. [Driver-Specific Dropping](#driver-specific-dropping) +3. [Follower-Specific Dropping](#follower-specific-dropping) +4. [Boundary and Proof Elements](#boundary-and-proof-elements) +5. [Synchronizer-Level Dropping](#synchronizer-level-dropping) +6. [Monitoring and Debugging](#monitoring-and-debugging) + +--- + +## Categories of Message Dropping + +Messages can be dropped for several reasons: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Message Dropping Categories │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. INTENTIONAL DROPPING (by design) │ +│ ├─ Throttled driver rate limiting │ +│ ├─ AnyBefore/AnyAtOrBefore non-deterministic selection │ +│ └─ Batch overlap (only oldest removed per capture) │ +│ │ +│ 2. BOUNDARY CLEANUP (after successful capture) │ +│ ├─ Elements older than captured range │ +│ ├─ "Proof" elements that enabled capture │ +│ └─ Elements no longer needed for future captures │ +│ │ +│ 3. ABORT SCENARIOS (data loss) │ +│ ├─ MatchedStamp missing exact match │ +│ ├─ ClosestBefore gap in window │ +│ └─ CountBefore insufficient count │ +│ │ +│ 4. QUEUE OVERFLOW (application-level) │ +│ └─ Custom queue limits in application code │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Driver-Specific Dropping + +### Next Driver: Minimal Dropping + +```cpp +driver::Next, NoLock> driver; +``` + +**Behavior:** Removes exactly ONE element per capture. + +``` +CAPTURE SEQUENCE: + Queue: [100, 200, 300, 400, 500] + + Capture 1: Removes 100 → Queue: [200, 300, 400, 500] + Capture 2: Removes 200 → Queue: [300, 400, 500] + Capture 3: Removes 300 → Queue: [400, 500] + + TOTAL DROPPED: 0 + TOTAL CAPTURED: 3 + + ✓ Every message is captured +``` + +### Batch Driver: Overlap Dropping + +```cpp +driver::Batch, NoLock> driver{5}; // batch size 5 +``` + +**Behavior:** Captures N elements, removes only the OLDEST. + +``` +CAPTURE SEQUENCE: + Queue: [0, 10, 20, 30, 40, 50, 60, 70, 80] + + Capture 1: + ├─ Captures: [0, 10, 20, 30, 40] + ├─ Range: {0, 40} + ├─ Removes: 0 (only oldest) + └─ Queue after: [10, 20, 30, 40, 50, 60, 70, 80] + + Capture 2: + ├─ Captures: [10, 20, 30, 40, 50] ← stamps 10-40 RECAPTURED! + ├─ Range: {10, 50} + ├─ Removes: 10 + └─ Queue after: [20, 30, 40, 50, 60, 70, 80] + + OVERLAP PATTERN: + ├─ Capture 1: [0, 10, 20, 30, 40] + ├─ Capture 2: [10, 20, 30, 40, 50] + ├─ Capture 3: [20, 30, 40, 50, 60] + └─ ... + + Each message captured 5 times before final removal! + + TOTAL DROPPED: 0 (all captured, some multiple times) +``` + +### Chunk Driver: Complete Batch Dropping + +```cpp +driver::Chunk, NoLock> driver{5}; +``` + +**Behavior:** Captures N elements, removes ALL captured. + +``` +CAPTURE SEQUENCE: + Queue: [0, 10, 20, 30, 40, 50, 60, 70, 80] + + Capture 1: + ├─ Captures: [0, 10, 20, 30, 40] + ├─ Removes: [0, 10, 20, 30, 40] + └─ Queue after: [50, 60, 70, 80] + + Capture 2: + ├─ Queue size: 4 < 5 + └─ State: RETRY (waiting for more data) + + Queue grows: [50, 60, 70, 80, 90] + + Capture 2 (retry): + ├─ Captures: [50, 60, 70, 80, 90] + ├─ Removes: [50, 60, 70, 80, 90] + └─ Queue after: [] + + TOTAL DROPPED: 0 + TOTAL CAPTURED: 10 (in 2 batches) + + ✓ Every message captured exactly once +``` + +### Throttled Driver: Intentional Dropping + +```cpp +driver::Throttled, NoLock> driver{100}; // period 100 +``` + +**Behavior:** Captures at target rate, skips intermediate data. + +``` +CAPTURE SEQUENCE: + Queue: [0, 10, 20, 30, ..., 990] // 100 elements at 10ms intervals + + Initial: last_captured_ = MIN (effectively -∞) + + Capture 1: + ├─ target = MIN + 100 ≈ MIN + ├─ First element >= MIN: stamp 0 + ├─ Captures: [Dispatch{0}] + ├─ Removes: [0] + └─ last_captured_ = 0 + + Capture 2: + ├─ target = 0 + 100 = 100 + ├─ First element >= 100: stamp 100 + ├─ Captures: [Dispatch{100}] + ├─ Removes: [10, 20, 30, ..., 100] ← 10 elements! + │ + │ ⚠️ DROPPED: stamps 10, 20, 30, 40, 50, 60, 70, 80, 90 + │ These fell within the throttle period + │ + └─ last_captured_ = 100 + + Capture 3: + ├─ target = 100 + 100 = 200 + ├─ Captures: [Dispatch{200}] + ├─ Removes: [110, 120, ..., 200] + │ + │ ⚠️ DROPPED: 9 more elements + │ + └─ last_captured_ = 200 + + FINAL STATISTICS: + ├─ Captured: 0, 100, 200, 300, ..., 900 (10 elements) + ├─ Dropped: 10, 20, 30, ..., 90, 110, 120, ... (90 elements) + │ + ├─ Capture rate: 10% + └─ Drop rate: 90% + + ⚠️ This is BY DESIGN for rate limiting! +``` + +### Throttle Period vs Data Rate + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Throttle Period Selection │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Data Rate: 100 Hz (10ms between samples) │ +│ │ +│ ┌────────────────┬──────────────┬───────────────┐ │ +│ │ Throttle Period│ Output Rate │ Drop Rate │ │ +│ ├────────────────┼──────────────┼───────────────┤ │ +│ │ 10ms │ 100 Hz │ 0% (all kept) │ │ +│ │ 20ms │ 50 Hz │ 50% │ │ +│ │ 50ms │ 20 Hz │ 80% │ │ +│ │ 100ms │ 10 Hz │ 90% │ │ +│ │ 500ms │ 2 Hz │ 98% │ │ +│ └────────────────┴──────────────┴───────────────┘ │ +│ │ +│ Rule: Drop Rate = 1 - (Data_Interval / Throttle_Period) │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Follower-Specific Dropping + +### Before: Drops All Captured + Proof + +```cpp +follower::Before, NoLock> follower{0}; +``` + +``` +CAPTURE: + Queue: [50, 80, 100, 150] + Driver range: {100, 100} + Boundary: 100 + + Locate: + ├─ Elements < 100: [50, 80] ← CAPTURED + ├─ Proof element >= 100: 100 ← PROOF (not captured) + └─ State: PRIMED + + After capture: + ├─ Removes: [50, 80] (captured) + └─ Queue: [100, 150] + + Stamp 100 retained (might be proof for next capture) +``` + +### ClosestBefore: Drops Window + Older + +```cpp +follower::ClosestBefore, NoLock> follower{20, 0}; +``` + +``` +CAPTURE: + Queue: [50, 70, 85, 95, 100, 110] + Driver range: {100, 100} + Boundary: 100, Window: [80, 100) + + Locate: + ├─ In window [80, 100): [85, 95] + ├─ Closest to 100: stamp 95 ← CAPTURED + └─ State: PRIMED + + After capture: + ├─ Removes up to boundary: [50, 70, 85, 95] + │ + │ ⚠️ DROPPED: 50, 70, 85 (older than closest) + │ These were in queue but not captured + │ + └─ Queue: [100, 110] + + Only stamp 95 was "used" - others dropped! +``` + +### CountBefore: Drops Excess Historical + +```cpp +follower::CountBefore, NoLock> follower{3, 0}; +``` + +``` +CAPTURE: + Queue: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110] + Driver range: {100, 100} + Boundary: 100 + + Locate: + ├─ Elements < 100: [10, 20, 30, 40, 50, 60, 70, 80, 90] + ├─ Need: 3 elements + ├─ Select LAST 3: [70, 80, 90] ← CAPTURED + └─ State: PRIMED + + After capture: + ├─ Removes up to boundary: all stamps < 100 + │ + │ ⚠️ DROPPED: 10, 20, 30, 40, 50, 60 (excess historical) + │ These existed but weren't part of the count + │ + └─ Queue: [100, 110] +``` + +### Ranged: Keeps Boundary Elements + +```cpp +follower::Ranged, NoLock> follower{0}; +``` + +``` +CAPTURE: + Queue: [70, 80, 90, 100, 110, 120] + Driver range: {90, 100} + Boundary: 90 + + Locate: + ├─ Need: 1 before 90, elements in [90,100], 1 after 100 + │ + ├─ Before 90: stamp 80 ← CAPTURED (lower bound) + ├─ In [90, 100]: [90, 100] ← CAPTURED + ├─ After 100: stamp 110 ← CAPTURED (upper bound) + │ + └─ Captured: [80, 90, 100, 110] + + After capture: + ├─ Removes only stamps < 80 (lower bound) + │ + │ ⚠️ DROPPED: 70 (before lower bound) + │ + └─ Queue: [80, 90, 100, 110, 120] + + Note: 80 kept for potential next capture's upper bound! +``` + +### AnyBefore: Non-Deterministic Capture + +```cpp +follower::AnyBefore, NoLock> follower{0}; +``` + +``` +CAPTURE: + Queue: [50, 60, 70, 80, 90, 100] + Driver range: {100, 100} + Boundary: 100 + + Locate: + ├─ Elements < 100: [50, 60, 70, 80, 90] + │ + │ Implementation may capture: + │ - ALL elements < boundary + │ - Just ONE element < boundary + │ - SOME elements < boundary + │ + │ ⚠️ Behavior varies by implementation! + │ + └─ State: PRIMED (even if empty) + + Typical after capture: + ├─ Removes all <= boundary + │ + │ ⚠️ Some elements may be CAPTURED + │ ⚠️ Some elements may be DROPPED (not returned) + │ depending on implementation + │ + └─ Queue: [100] + + Use AnyBefore when: + - You only need to know IF data exists before + - You can handle non-deterministic capture + - Missing data is acceptable +``` + +### Latched: Historical Dropping + +```cpp +follower::Latched, NoLock> follower{100}; +``` + +``` +CAPTURE: + Queue: [0, 50, 100, 150] + Driver range: {200, 200} + Boundary: 200 - 100 = 100 + + Locate: + ├─ Find stamp <= 100: stamps 0, 50, 100 + ├─ Select MOST RECENT: stamp 100 ← CAPTURED + ├─ Update latched_: Dispatch{100} + └─ State: PRIMED + + After capture: + ├─ Removes stamps <= 100: [0, 50, 100] + │ + │ ⚠️ DROPPED: 0, 50 (superseded by 100) + │ Older values no longer needed + │ + └─ Queue: [150] + +SUBSEQUENT CAPTURE: + Queue: [150] (no new config) + Driver range: {300, 300} + Boundary: 200 + + Locate: + ├─ Find stamp <= 200: NONE in queue + ├─ Use latched_: Dispatch{100} + └─ State: PRIMED + + OUTPUT: Returns latched value, nothing dropped +``` + +--- + +## Boundary and Proof Elements + +### What is a "Proof" Element? + +A proof element demonstrates that no more data will arrive before a boundary. + +``` +PROOF CONCEPT: + + Assumption: Timestamps are monotonically increasing + + If queue contains element with stamp >= boundary, + then NO future element can have stamp < boundary + + ├─ Current queue: [50, 80, 120, 150] + ├─ Boundary: 100 + ├─ Proof element: 120 (first >= 100) + │ + └─ Conclusion: [50, 80] is ALL data < 100 + (Any future inject will have stamp > newest = 150) +``` + +### Proof Element Retention + +``` +SCENARIO: Multiple captures sharing proof + + Queue: [80, 100, 120] + + Capture 1 (boundary = 90): + ├─ Captured: [80] + ├─ Proof: 100 + └─ Queue after: [100, 120] // 100 retained! + + Capture 2 (boundary = 110): + ├─ Captured: [100] // Previously proof, now data + ├─ Proof: 120 + └─ Queue after: [120] // 120 retained! + + The proof element from capture N may become + captured data in capture N+1 +``` + +--- + +## Synchronizer-Level Dropping + +### ABORT Causes Coordinated Dropping + +```cpp +Synchronizer<...> sync{driver, follower1, follower2}; +``` + +``` +SCENARIO: + Driver queue: [100, 200, 300] + Follower 1: [100, 200, 300] + Follower 2: [100, 300] // MISSING 200 + +CAPTURE for t=200: + Driver: range = {200, 200}, ready to capture + Follower 1: ready to capture + Follower 2: ABORT (missing stamp 200) + + SYNCHRONIZER: + ├─ State = ABORT (follower 2 failed) + ├─ All captors release data for range {200, 200} + │ + │ Driver: removes stamp 200 + │ Follower 1: removes stamp 200 + │ Follower 2: removes stamps up to 200 (if any) + │ + └─ Data for t=200 DROPPED from all captors + + ⚠️ Even though driver and follower 1 HAD the data, + it's dropped because follower 2 couldn't sync +``` + +### Partial vs Complete Drops + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Drop Behavior by Synchronization State │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ PRIMED (success): │ +│ ├─ Driver data: CAPTURED and removed │ +│ └─ Follower data: CAPTURED and removed │ +│ │ +│ ABORT (failure): │ +│ ├─ Driver data: DROPPED (removed without capture) │ +│ └─ Follower data: DROPPED (removed without capture) │ +│ │ +│ RETRY (waiting): │ +│ ├─ Driver data: RETAINED │ +│ └─ Follower data: RETAINED │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Monitoring and Debugging + +### Tracking Dropped Messages + +```cpp +// Custom captor wrapper with drop tracking +template +class MonitoredCaptor : public Captor { +public: + size_t injected_count = 0; + size_t captured_count = 0; + size_t dropped_count = 0; + + void inject(auto stamp, auto data) { + injected_count++; + Captor::inject(stamp, data); + } + + // Override capture methods to track + // captured_count and dropped_count + + void report() { + std::cout << "Injected: " << injected_count << "\n" + << "Captured: " << captured_count << "\n" + << "Dropped: " << dropped_count << "\n" + << "Drop Rate: " + << (100.0 * dropped_count / injected_count) + << "%\n"; + } +}; +``` + +### Debug Output Example + +```cpp +#include +#include +#include + +// Enable streaming for debug +std::cout << "Synchronizer state: " << sync << std::endl; +``` + +### Common Drop Patterns to Watch + +``` +1. HIGH ABORT RATE + ├─ Symptom: Many frames dropped + ├─ Cause: Mismatched timestamps, missing data + └─ Fix: Use flexible followers (AnyBefore, ClosestBefore) + +2. GROWING QUEUE SIZE + ├─ Symptom: Memory usage increases + ├─ Cause: RETRY loops, slow processing + └─ Fix: Add throttling, increase processing rate + +3. UNEXPECTED EMPTY CAPTURES + ├─ Symptom: Before/AnyBefore return empty + ├─ Cause: Wrong delay/period configuration + └─ Fix: Match config to actual data timing + +4. PERIODIC ABORTS + ├─ Symptom: Regular pattern of drops + ├─ Cause: Timing mismatch in periodic data + └─ Fix: Adjust period to match actual data rate +``` + +--- + +## Summary: Drop Behavior by Captor Type + +| Captor | What Gets Dropped | Why | +|--------|-------------------|-----| +| `driver::Next` | Captured element only | One-at-a-time processing | +| `driver::Batch` | Only oldest per capture | Sliding window design | +| `driver::Chunk` | All captured elements | Complete batch processing | +| `driver::Throttled` | Elements within throttle period | Rate limiting by design | +| `follower::Before` | All < boundary | Already captured or stale | +| `follower::ClosestBefore` | All < boundary | Only closest needed | +| `follower::CountBefore` | All < boundary | Only last N needed | +| `follower::Ranged` | Only < lower bound | Bounds kept for context | +| `follower::MatchedStamp` | Captured element only | Exact match required | +| `follower::Latched` | All <= boundary | Only latest needed | +| `follower::AnyBefore` | Implementation-defined | Flexible capture | +| `follower::AnyAtOrBefore` | Implementation-defined | Flexible capture | + +### Key Takeaways + +1. **Throttled is the main intentional dropper** - use it only when rate limiting is desired +2. **ClosestBefore and CountBefore drop non-selected elements** - older data is discarded +3. **ABORT causes all captors to drop** - synchronization failure loses entire frame +4. **Proof elements are retained** - they become data in subsequent captures +5. **Configure periods/delays carefully** - mismatched config causes unexpected drops