Skip to content

feat(service): implement real auto-switch metrics and adapter lifecycle#28

Open
EffortlessSteven wants to merge 2 commits intomainfrom
feat/autoswitch-metrics
Open

feat(service): implement real auto-switch metrics and adapter lifecycle#28
EffortlessSteven wants to merge 2 commits intomainfrom
feat/autoswitch-metrics

Conversation

@EffortlessSteven
Copy link
Member

Summary

Adds a lock-free \AutoSwitchMetrics\ struct and an \AdapterDetectionHooks\ trait to the auto-switch service, replacing conceptual TODOs with real implementations.

Changes

AutoSwitchMetrics (lock-free aggregate counters)

  • \switches_total\ — total profile switches performed
  • \detection_attempts\ — total detection attempts (success + failure)
  • \detection_failures\ — total failed detections
  • \�vg_detection_us\ — exponential moving average detection time (µs)

  • ecord_switch(duration)\ — record a successful switch with timing

  • ecord_failure()\ — record a failed detection attempt
  • \snapshot()\ — returns \HashMap<String, u64>\ with all counters

AdapterDetectionHooks trait

  • \on_detection_start(sim)\ — called when process detection triggers adapter init
  • \on_detection_stop(sim)\ — called when process is lost and adapter tears down
  • \�dapter_state()\ — returns current adapter lifecycle state
  • Default implementations return \Ok(())\ / \AdapterState::Stopped\

Tests (16 new)

  • Zeroed initial state, default impl, counter increments
  • EMA convergence, snapshot key completeness, ms conversion
  • Mixed success/failure accounting, Debug format
  • Hook lifecycle (start/stop), multi-sim hooks, failure paths

Validation

  • \cargo test -p flight-service --lib\ — 425 tests pass
  • \cargo clippy -p flight-service -- -D warnings\ — zero warnings

- Add AutoSwitchMetrics struct with AtomicU64 counters (switches_total,
  detection_attempts, detection_failures, avg_detection_ms)
- Add record_switch() for recording switch events with timing (EMA)
- Add record_failure() for failed detection attempts
- Add snapshot() returning HashMap<String, u64> metrics snapshot
- Add AdapterDetectionHooks trait for adapter lifecycle callbacks
  (on_detection_start, on_detection_stop, adapter_state)
- Add 16 new tests for metrics tracking, EMA convergence, snapshot keys,
  mixed success/failure accounting, hook lifecycle, and failure paths

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 1, 2026 00:35
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@gemini-code-assist
Copy link

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

Warning

Rate limit exceeded

@EffortlessSteven has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 28 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between cd2e54d and c026bce.

📒 Files selected for processing (1)
  • crates/flight-service/src/aircraft_auto_switch_service.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/autoswitch-metrics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-free-for-open-source-projects

Review Summary by Qodo

Add AutoSwitchMetrics and AdapterDetectionHooks implementations

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Implement lock-free AutoSwitchMetrics struct with atomic counters
  - Tracks switches_total, detection_attempts, detection_failures
  - Computes exponential-moving-average detection time in microseconds
  - Provides snapshot() method returning HashMap with millisecond conversion
• Add AdapterDetectionHooks trait for adapter lifecycle management
  - Callbacks for on_detection_start and on_detection_stop events
  - Default implementations return Ok(()) and AdapterState::Stopped
  - Enables setup/teardown of expensive resources (connections, sockets)
• Add 16 comprehensive tests covering metrics and hooks
  - Zeroed initial state, counter increments, EMA convergence
  - Snapshot key completeness and millisecond conversion
  - Hook lifecycle, multi-simulator support, failure paths
Diagram
flowchart LR
  A["AutoSwitchMetrics<br/>Lock-free counters"] -->|record_switch| B["EMA Detection Time<br/>Microseconds"]
  A -->|record_failure| C["Failure Tracking"]
  A -->|snapshot| D["HashMap<String, u64><br/>Millisecond conversion"]
  E["AdapterDetectionHooks<br/>Trait"] -->|on_detection_start| F["Adapter Setup"]
  E -->|on_detection_stop| G["Adapter Teardown"]
  E -->|adapter_state| H["State Query"]
Loading

Grey Divider

File Changes

1. crates/flight-service/src/aircraft_auto_switch_service.rs Enhancement, tests +380/-0

Implement metrics and adapter lifecycle hooks

• Add AutoSwitchMetrics struct with AtomicU64 counters for switches_total, detection_attempts,
 detection_failures, and avg_detection_us
• Implement record_switch() method with integer EMA calculation (α=0.1) for detection timing
• Implement record_failure() method to track failed detection attempts
• Implement snapshot() method returning HashMap with millisecond conversion for avg_detection_ms
• Add Default and Debug trait implementations for AutoSwitchMetrics
• Add AdapterDetectionHooks trait with on_detection_start, on_detection_stop, and adapter_state
 methods
• Add 16 new unit tests covering metrics initialization, counter increments, EMA convergence,
 snapshot validation, mixed success/failure accounting, hook lifecycle, and error handling

crates/flight-service/src/aircraft_auto_switch_service.rs


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Mar 1, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. New APIs not wired🐞 Bug ⛯ Reliability
Description
AutoSwitchMetrics and AdapterDetectionHooks are added as public building blocks but are not
stored or invoked anywhere in AircraftAutoSwitchService, so they currently provide no runtime
behavior and can mislead consumers/maintainers about feature completeness. If the PR goal is “real
auto-switch metrics and adapter lifecycle”, integration is missing.
Code

crates/flight-service/src/aircraft_auto_switch_service.rs[R295-443]

+/// Lock-free aggregate metrics for the auto-switch subsystem.
+///
+/// All counters use [`AtomicU64`] with relaxed ordering so they can be
+/// updated from any task without acquiring an async lock.
+pub struct AutoSwitchMetrics {
+    /// Total number of profile switches performed.
+    switches_total: AtomicU64,
+    /// Total detection attempts (successful + failed).
+    detection_attempts: AtomicU64,
+    /// Total detection failures.
+    detection_failures: AtomicU64,
+    /// Exponential-moving-average detection time stored as whole
+    /// microseconds (µs). Callers that want milliseconds can divide by 1000.
+    avg_detection_us: AtomicU64,
+}
+
+impl AutoSwitchMetrics {
+    /// Create a new, zeroed metrics instance.
+    pub fn new() -> Self {
+        Self {
+            switches_total: AtomicU64::new(0),
+            detection_attempts: AtomicU64::new(0),
+            detection_failures: AtomicU64::new(0),
+            avg_detection_us: AtomicU64::new(0),
+        }
+    }
+
+    /// Record a successful switch event with the given detection duration.
+    ///
+    /// Updates `switches_total`, `detection_attempts`, and the running EMA of
+    /// detection time.
+    pub fn record_switch(&self, detection_duration: Duration) {
+        self.switches_total.fetch_add(1, Ordering::Relaxed);
+        self.detection_attempts.fetch_add(1, Ordering::Relaxed);
+
+        // EMA with α = 0.1, operating in integer microseconds.
+        let sample_us = detection_duration.as_micros() as u64;
+        let _ = self.avg_detection_us.fetch_update(
+            Ordering::Relaxed,
+            Ordering::Relaxed,
+            |old| {
+                if old == 0 {
+                    Some(sample_us)
+                } else {
+                    // Integer EMA: new = old + (sample - old) / 10
+                    let diff = sample_us as i64 - old as i64;
+                    Some((old as i64 + diff / 10) as u64)
+                }
+            },
+        );
+    }
+
+    /// Record a failed detection attempt.
+    pub fn record_failure(&self) {
+        self.detection_attempts.fetch_add(1, Ordering::Relaxed);
+        self.detection_failures.fetch_add(1, Ordering::Relaxed);
+    }
+
+    /// Return a snapshot of all counters as a `HashMap<String, u64>`.
+    ///
+    /// Keys: `"switches_total"`, `"detection_attempts"`,
+    /// `"detection_failures"`, `"avg_detection_ms"`.
+    pub fn snapshot(&self) -> HashMap<String, u64> {
+        let mut map = HashMap::with_capacity(4);
+        map.insert(
+            "switches_total".into(),
+            self.switches_total.load(Ordering::Relaxed),
+        );
+        map.insert(
+            "detection_attempts".into(),
+            self.detection_attempts.load(Ordering::Relaxed),
+        );
+        map.insert(
+            "detection_failures".into(),
+            self.detection_failures.load(Ordering::Relaxed),
+        );
+        map.insert(
+            "avg_detection_ms".into(),
+            self.avg_detection_us.load(Ordering::Relaxed) / 1000,
+        );
+        map
+    }
+
+    /// Read individual counters.
+    pub fn switches_total(&self) -> u64 {
+        self.switches_total.load(Ordering::Relaxed)
+    }
+
+    pub fn detection_attempts(&self) -> u64 {
+        self.detection_attempts.load(Ordering::Relaxed)
+    }
+
+    pub fn detection_failures(&self) -> u64 {
+        self.detection_failures.load(Ordering::Relaxed)
+    }
+
+    /// Average detection time in microseconds.
+    pub fn avg_detection_us(&self) -> u64 {
+        self.avg_detection_us.load(Ordering::Relaxed)
+    }
+}
+
+impl Default for AutoSwitchMetrics {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl std::fmt::Debug for AutoSwitchMetrics {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("AutoSwitchMetrics")
+            .field("switches_total", &self.switches_total())
+            .field("detection_attempts", &self.detection_attempts())
+            .field("detection_failures", &self.detection_failures())
+            .field("avg_detection_us", &self.avg_detection_us())
+            .finish()
+    }
+}
+
+// ============================================================================
+// AdapterDetectionHooks — trait for adapter lifecycle callbacks
+// ============================================================================
+
+/// Trait for adapter detection lifecycle hooks.
+///
+/// Implementors receive callbacks when the auto-switch service starts or stops
+/// detection for a particular simulator. This allows adapters to perform
+/// setup/teardown (e.g. opening a SimConnect handle, binding a UDP socket)
+/// exactly when needed.
+#[allow(unused_variables)]
+pub trait AdapterDetectionHooks: Send + Sync {
+    /// Called when process detection triggers adapter initialisation.
+    ///
+    /// The adapter should perform any expensive setup here (connections,
+    /// allocations, background tasks).  Returns `Ok(())` on success.
+    fn on_detection_start(&mut self, sim: BusSimId) -> std::result::Result<(), String> {
+        Ok(())
+    }
+
+    /// Called when the process is lost and the adapter should tear down.
+    fn on_detection_stop(&mut self, sim: BusSimId) -> std::result::Result<(), String> {
+        Ok(())
+    }
+
+    /// Return the current adapter state (informational).
+    fn adapter_state(&self) -> AdapterState {
+        AdapterState::Stopped
+    }
+}
Evidence
The service constructor initializes only existing atomics and adapter metrics; no field exists for
AutoSwitchMetrics or hook implementors. The event loop handles process detected/lost and adapter
init/teardown without invoking AdapterDetectionHooks, and get_metrics() reads flight-session’s
SwitchMetrics and the existing atomics, never referencing AutoSwitchMetrics.

crates/flight-service/src/aircraft_auto_switch_service.rs[465-506]
crates/flight-service/src/aircraft_auto_switch_service.rs[547-607]
crates/flight-service/src/aircraft_auto_switch_service.rs[920-1015]
crates/flight-service/src/aircraft_auto_switch_service.rs[738-758]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`AutoSwitchMetrics` and `AdapterDetectionHooks` are defined and tested, but the service never stores or invokes them. This leaves the PR’s stated feature (“real auto-switch metrics and adapter lifecycle”) incomplete.
### Issue Context
Current service metrics are sourced from `flight-session` (`SwitchMetrics`) and existing atomics; adapter start/stop is handled directly without hooks.
### Fix Focus Areas
- crates/flight-service/src/aircraft_auto_switch_service.rs[295-443]
- crates/flight-service/src/aircraft_auto_switch_service.rs[465-506]
- crates/flight-service/src/aircraft_auto_switch_service.rs[547-607]
- crates/flight-service/src/aircraft_auto_switch_service.rs[738-758]
- crates/flight-service/src/aircraft_auto_switch_service.rs[920-1015]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Attempts counter semantics🐞 Bug ✓ Correctness
Description
AutoSwitchMetrics documents detection_attempts as “successful + failed” detection attempts, but
the only success path is record_switch, conflating “successful detection” with “switch performed”
and making it impossible to count successful detections that don’t switch (which exist in
flight-session). This will under-report detection activity and skew the EMA once the struct is
integrated.
Code

crates/flight-service/src/aircraft_auto_switch_service.rs[R299-351]

+pub struct AutoSwitchMetrics {
+    /// Total number of profile switches performed.
+    switches_total: AtomicU64,
+    /// Total detection attempts (successful + failed).
+    detection_attempts: AtomicU64,
+    /// Total detection failures.
+    detection_failures: AtomicU64,
+    /// Exponential-moving-average detection time stored as whole
+    /// microseconds (µs). Callers that want milliseconds can divide by 1000.
+    avg_detection_us: AtomicU64,
+}
+
+impl AutoSwitchMetrics {
+    /// Create a new, zeroed metrics instance.
+    pub fn new() -> Self {
+        Self {
+            switches_total: AtomicU64::new(0),
+            detection_attempts: AtomicU64::new(0),
+            detection_failures: AtomicU64::new(0),
+            avg_detection_us: AtomicU64::new(0),
+        }
+    }
+
+    /// Record a successful switch event with the given detection duration.
+    ///
+    /// Updates `switches_total`, `detection_attempts`, and the running EMA of
+    /// detection time.
+    pub fn record_switch(&self, detection_duration: Duration) {
+        self.switches_total.fetch_add(1, Ordering::Relaxed);
+        self.detection_attempts.fetch_add(1, Ordering::Relaxed);
+
+        // EMA with α = 0.1, operating in integer microseconds.
+        let sample_us = detection_duration.as_micros() as u64;
+        let _ = self.avg_detection_us.fetch_update(
+            Ordering::Relaxed,
+            Ordering::Relaxed,
+            |old| {
+                if old == 0 {
+                    Some(sample_us)
+                } else {
+                    // Integer EMA: new = old + (sample - old) / 10
+                    let diff = sample_us as i64 - old as i64;
+                    Some((old as i64 + diff / 10) as u64)
+                }
+            },
+        );
+    }
+
+    /// Record a failed detection attempt.
+    pub fn record_failure(&self) {
+        self.detection_attempts.fetch_add(1, Ordering::Relaxed);
+        self.detection_failures.fetch_add(1, Ordering::Relaxed);
+    }
Evidence
The struct claims detection_attempts tracks all attempts, but the API only increments it via
record_switch (which also increments switches_total) and record_failure, meaning
attempts==switches+failures. Separately, the core auto-switch logic explicitly ignores some
detections without switching, so a correct “attempts” counter needs a way to record
successful-but-no-switch detections (or the documentation/naming must change).

crates/flight-service/src/aircraft_auto_switch_service.rs[295-351]
crates/flight-session/src/lib.rs[671-689]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`AutoSwitchMetrics.detection_attempts` is documented as counting total detection attempts (successful + failed), but the only success API (`record_switch`) inherently implies a switch occurred. In this codebase, detections can be processed and intentionally ignored without a switch, so the metric cannot represent what its documentation claims.
### Issue Context
This becomes problematic once metrics are integrated/exported: dashboards/alerts will under-count detection load and skew averages.
### Fix Focus Areas
- crates/flight-service/src/aircraft_auto_switch_service.rs[295-351]
- crates/flight-session/src/lib.rs[671-689]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Hooks encourage blocking 🐞 Bug ⛯ Reliability
Description
AdapterDetectionHooks explicitly instructs implementors to do “expensive setup” in
on_detection_start, but the hook is synchronous, so a naive implementation may block the Tokio
runtime thread during adapter start/stop. This is especially risky given the service already
performs async adapter startup work.
Code

crates/flight-service/src/aircraft_auto_switch_service.rs[R418-437]

+/// Trait for adapter detection lifecycle hooks.
+///
+/// Implementors receive callbacks when the auto-switch service starts or stops
+/// detection for a particular simulator. This allows adapters to perform
+/// setup/teardown (e.g. opening a SimConnect handle, binding a UDP socket)
+/// exactly when needed.
+#[allow(unused_variables)]
+pub trait AdapterDetectionHooks: Send + Sync {
+    /// Called when process detection triggers adapter initialisation.
+    ///
+    /// The adapter should perform any expensive setup here (connections,
+    /// allocations, background tasks).  Returns `Ok(())` on success.
+    fn on_detection_start(&mut self, sim: BusSimId) -> std::result::Result<(), String> {
+        Ok(())
+    }
+
+    /// Called when the process is lost and the adapter should tear down.
+    fn on_detection_stop(&mut self, sim: BusSimId) -> std::result::Result<(), String> {
+        Ok(())
+    }
Evidence
The hook docs recommend expensive work, but the signature is sync (fn ... -> Result), so there’s
no natural way to await non-blocking I/O; if implementors follow the doc literally, they may perform
blocking work on the async executor thread. The service’s existing adapter lifecycle already uses
async startup (adapter.start().await) which underscores that lifecycle operations are async in
practice.

crates/flight-service/src/aircraft_auto_switch_service.rs[418-437]
crates/flight-service/src/aircraft_auto_switch_service.rs[1017-1030]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The hook docs instruct implementors to do “expensive setup” inside synchronous methods, which invites blocking behavior on Tokio runtime threads.
### Issue Context
Adapter lifecycle in this service already uses async work (`start().await` in spawned tasks), so hooks likely need to participate in async flows without blocking.
### Fix Focus Areas
- crates/flight-service/src/aircraft_auto_switch_service.rs[418-437]
- crates/flight-service/src/aircraft_auto_switch_service.rs[1017-1030]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new building blocks in flight-service’s aircraft auto-switch subsystem: a lock-free AutoSwitchMetrics counter aggregate and an AdapterDetectionHooks trait intended for adapter lifecycle callbacks, along with unit tests validating both APIs.

Changes:

  • Introduce AutoSwitchMetrics backed by AtomicU64 counters plus snapshot/debug helpers.
  • Add AdapterDetectionHooks trait with default start/stop/state methods.
  • Add unit tests covering counter behavior, EMA behavior, snapshot completeness, and hook lifecycle/failure paths.

Comment on lines +331 to +342
let sample_us = detection_duration.as_micros() as u64;
let _ = self.avg_detection_us.fetch_update(
Ordering::Relaxed,
Ordering::Relaxed,
|old| {
if old == 0 {
Some(sample_us)
} else {
// Integer EMA: new = old + (sample - old) / 10
let diff = sample_us as i64 - old as i64;
Some((old as i64 + diff / 10) as u64)
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

record_switch computes the EMA using sample_us as i64 / old as i64. Casting u64i64 can wrap for values > i64::MAX, producing a negative diff and corrupting the average. Consider doing the EMA math in i128/u128 (or using checked/saturating conversions) and only converting back to u64 after clamping, to avoid wraparound/truncation edge cases.

Copilot uses AI. Check for mistakes.
Comment on lines +424 to +435
#[allow(unused_variables)]
pub trait AdapterDetectionHooks: Send + Sync {
/// Called when process detection triggers adapter initialisation.
///
/// The adapter should perform any expensive setup here (connections,
/// allocations, background tasks). Returns `Ok(())` on success.
fn on_detection_start(&mut self, sim: BusSimId) -> std::result::Result<(), String> {
Ok(())
}

/// Called when the process is lost and the adapter should tear down.
fn on_detection_stop(&mut self, sim: BusSimId) -> std::result::Result<(), String> {
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#[allow(unused_variables)] on the whole trait suppresses warnings for all implementors. Since only the default method bodies ignore sim, prefer naming the parameter _sim in the default implementations (or using let _ = sim;) and remove the broad allow attribute so real unused-variable issues in implementors aren’t hidden.

Suggested change
#[allow(unused_variables)]
pub trait AdapterDetectionHooks: Send + Sync {
/// Called when process detection triggers adapter initialisation.
///
/// The adapter should perform any expensive setup here (connections,
/// allocations, background tasks). Returns `Ok(())` on success.
fn on_detection_start(&mut self, sim: BusSimId) -> std::result::Result<(), String> {
Ok(())
}
/// Called when the process is lost and the adapter should tear down.
fn on_detection_stop(&mut self, sim: BusSimId) -> std::result::Result<(), String> {
pub trait AdapterDetectionHooks: Send + Sync {
/// Called when process detection triggers adapter initialisation.
///
/// The adapter should perform any expensive setup here (connections,
/// allocations, background tasks). Returns `Ok(())` on success.
fn on_detection_start(&mut self, _sim: BusSimId) -> std::result::Result<(), String> {
Ok(())
}
/// Called when the process is lost and the adapter should tear down.
fn on_detection_stop(&mut self, _sim: BusSimId) -> std::result::Result<(), String> {

Copilot uses AI. Check for mistakes.
Comment on lines +295 to +326
/// Lock-free aggregate metrics for the auto-switch subsystem.
///
/// All counters use [`AtomicU64`] with relaxed ordering so they can be
/// updated from any task without acquiring an async lock.
pub struct AutoSwitchMetrics {
/// Total number of profile switches performed.
switches_total: AtomicU64,
/// Total detection attempts (successful + failed).
detection_attempts: AtomicU64,
/// Total detection failures.
detection_failures: AtomicU64,
/// Exponential-moving-average detection time stored as whole
/// microseconds (µs). Callers that want milliseconds can divide by 1000.
avg_detection_us: AtomicU64,
}

impl AutoSwitchMetrics {
/// Create a new, zeroed metrics instance.
pub fn new() -> Self {
Self {
switches_total: AtomicU64::new(0),
detection_attempts: AtomicU64::new(0),
detection_failures: AtomicU64::new(0),
avg_detection_us: AtomicU64::new(0),
}
}

/// Record a successful switch event with the given detection duration.
///
/// Updates `switches_total`, `detection_attempts`, and the running EMA of
/// detection time.
pub fn record_switch(&self, detection_duration: Duration) {
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoSwitchMetrics and AdapterDetectionHooks are introduced as pub, but they don’t appear to be used anywhere in this module (no calls to record_switch/record_failure, and no code invokes the hook callbacks). If the intent is to implement “real auto-switch metrics and adapter lifecycle”, it looks incomplete—either wire these into the service’s detection/switch paths or adjust the PR scope/visibility so the new APIs aren’t dead weight.

Copilot uses AI. Check for mistakes.
Comment on lines +295 to +443
/// Lock-free aggregate metrics for the auto-switch subsystem.
///
/// All counters use [`AtomicU64`] with relaxed ordering so they can be
/// updated from any task without acquiring an async lock.
pub struct AutoSwitchMetrics {
/// Total number of profile switches performed.
switches_total: AtomicU64,
/// Total detection attempts (successful + failed).
detection_attempts: AtomicU64,
/// Total detection failures.
detection_failures: AtomicU64,
/// Exponential-moving-average detection time stored as whole
/// microseconds (µs). Callers that want milliseconds can divide by 1000.
avg_detection_us: AtomicU64,
}

impl AutoSwitchMetrics {
/// Create a new, zeroed metrics instance.
pub fn new() -> Self {
Self {
switches_total: AtomicU64::new(0),
detection_attempts: AtomicU64::new(0),
detection_failures: AtomicU64::new(0),
avg_detection_us: AtomicU64::new(0),
}
}

/// Record a successful switch event with the given detection duration.
///
/// Updates `switches_total`, `detection_attempts`, and the running EMA of
/// detection time.
pub fn record_switch(&self, detection_duration: Duration) {
self.switches_total.fetch_add(1, Ordering::Relaxed);
self.detection_attempts.fetch_add(1, Ordering::Relaxed);

// EMA with α = 0.1, operating in integer microseconds.
let sample_us = detection_duration.as_micros() as u64;
let _ = self.avg_detection_us.fetch_update(
Ordering::Relaxed,
Ordering::Relaxed,
|old| {
if old == 0 {
Some(sample_us)
} else {
// Integer EMA: new = old + (sample - old) / 10
let diff = sample_us as i64 - old as i64;
Some((old as i64 + diff / 10) as u64)
}
},
);
}

/// Record a failed detection attempt.
pub fn record_failure(&self) {
self.detection_attempts.fetch_add(1, Ordering::Relaxed);
self.detection_failures.fetch_add(1, Ordering::Relaxed);
}

/// Return a snapshot of all counters as a `HashMap<String, u64>`.
///
/// Keys: `"switches_total"`, `"detection_attempts"`,
/// `"detection_failures"`, `"avg_detection_ms"`.
pub fn snapshot(&self) -> HashMap<String, u64> {
let mut map = HashMap::with_capacity(4);
map.insert(
"switches_total".into(),
self.switches_total.load(Ordering::Relaxed),
);
map.insert(
"detection_attempts".into(),
self.detection_attempts.load(Ordering::Relaxed),
);
map.insert(
"detection_failures".into(),
self.detection_failures.load(Ordering::Relaxed),
);
map.insert(
"avg_detection_ms".into(),
self.avg_detection_us.load(Ordering::Relaxed) / 1000,
);
map
}

/// Read individual counters.
pub fn switches_total(&self) -> u64 {
self.switches_total.load(Ordering::Relaxed)
}

pub fn detection_attempts(&self) -> u64 {
self.detection_attempts.load(Ordering::Relaxed)
}

pub fn detection_failures(&self) -> u64 {
self.detection_failures.load(Ordering::Relaxed)
}

/// Average detection time in microseconds.
pub fn avg_detection_us(&self) -> u64 {
self.avg_detection_us.load(Ordering::Relaxed)
}
}

impl Default for AutoSwitchMetrics {
fn default() -> Self {
Self::new()
}
}

impl std::fmt::Debug for AutoSwitchMetrics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AutoSwitchMetrics")
.field("switches_total", &self.switches_total())
.field("detection_attempts", &self.detection_attempts())
.field("detection_failures", &self.detection_failures())
.field("avg_detection_us", &self.avg_detection_us())
.finish()
}
}

// ============================================================================
// AdapterDetectionHooks — trait for adapter lifecycle callbacks
// ============================================================================

/// Trait for adapter detection lifecycle hooks.
///
/// Implementors receive callbacks when the auto-switch service starts or stops
/// detection for a particular simulator. This allows adapters to perform
/// setup/teardown (e.g. opening a SimConnect handle, binding a UDP socket)
/// exactly when needed.
#[allow(unused_variables)]
pub trait AdapterDetectionHooks: Send + Sync {
/// Called when process detection triggers adapter initialisation.
///
/// The adapter should perform any expensive setup here (connections,
/// allocations, background tasks). Returns `Ok(())` on success.
fn on_detection_start(&mut self, sim: BusSimId) -> std::result::Result<(), String> {
Ok(())
}

/// Called when the process is lost and the adapter should tear down.
fn on_detection_stop(&mut self, sim: BusSimId) -> std::result::Result<(), String> {
Ok(())
}

/// Return the current adapter state (informational).
fn adapter_state(&self) -> AdapterState {
AdapterState::Stopped
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. New apis not wired 🐞 Bug ⛯ Reliability

AutoSwitchMetrics and AdapterDetectionHooks are added as public building blocks but are not
stored or invoked anywhere in AircraftAutoSwitchService, so they currently provide no runtime
behavior and can mislead consumers/maintainers about feature completeness. If the PR goal is “real
auto-switch metrics and adapter lifecycle”, integration is missing.
Agent Prompt
### Issue description
`AutoSwitchMetrics` and `AdapterDetectionHooks` are defined and tested, but the service never stores or invokes them. This leaves the PR’s stated feature (“real auto-switch metrics and adapter lifecycle”) incomplete.

### Issue Context
Current service metrics are sourced from `flight-session` (`SwitchMetrics`) and existing atomics; adapter start/stop is handled directly without hooks.

### Fix Focus Areas
- crates/flight-service/src/aircraft_auto_switch_service.rs[295-443]
- crates/flight-service/src/aircraft_auto_switch_service.rs[465-506]
- crates/flight-service/src/aircraft_auto_switch_service.rs[547-607]
- crates/flight-service/src/aircraft_auto_switch_service.rs[738-758]
- crates/flight-service/src/aircraft_auto_switch_service.rs[920-1015]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

…sues

- Fix EMA i64 overflow by computing in i128 with clamping
- Remove broad #[allow(unused_variables)], use _sim prefixes instead
- Wire AutoSwitchMetrics into AircraftAutoSwitchService
- Invoke AdapterDetectionHooks during detection lifecycle
- Add get_switch_metrics() and add_detection_hook() methods

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants