Skip to main content

telepath_server/
profile.rs

1//! DWT-based framing instrumentation (enabled by the `profile` feature).
2//!
3//! Accumulates cycle counts and byte counts for each COBS encode/decode
4//! operation in `process_frame`. The counters are stored in module-level
5//! statics so they survive across multiple poll() calls.
6//!
7//! Call [`init_dwt`] once at startup (done automatically by
8//! [`TelepathServer::new`] when `profile` is enabled) to enable the
9//! Cortex-M DWT cycle counter.
10//!
11//! Retrieve and atomically reset all counters by calling
12//! [`snapshot_and_reset`], or equivalently by sending CmdID `0xFFFE`
13//! (`CMD_ID_METRICS`) from the host.
14
15// On targets with native 64-bit atomics (e.g. x86_64 host), use core directly.
16// On 32-bit embedded targets (e.g. Cortex-M4), fall back to portable-atomic
17// which requires the caller to enable "unsafe-assume-single-core" + "fallback"
18// features in their own Cargo.toml (see examples/nrf52840-ping).
19#[cfg(target_has_atomic = "64")]
20use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
21#[cfg(not(target_has_atomic = "64"))]
22use portable_atomic::{AtomicU32, AtomicU64, Ordering};
23
24use telepath_wire::MetricsSnapshot;
25
26pub(crate) static ENCODE_CYCLES: AtomicU64 = AtomicU64::new(0);
27pub(crate) static DECODE_CYCLES: AtomicU64 = AtomicU64::new(0);
28pub(crate) static ENCODED_BYTES: AtomicU32 = AtomicU32::new(0);
29pub(crate) static DECODED_BYTES: AtomicU32 = AtomicU32::new(0);
30pub(crate) static SAMPLE_COUNT: AtomicU32 = AtomicU32::new(0);
31
32/// Enable the Cortex-M DWT cycle counter.
33///
34/// On ARM targets: sets `DEMCR.TRCENA`, enables the cycle counter, and resets
35/// `CYCCNT` to 0. Uses `Peripherals::steal` so user code does not need to give
36/// up the singleton. Idempotent — safe to call multiple times.
37///
38/// On non-ARM targets (e.g. x86_64 host-pty-server): no-op. The atomic counters
39/// still accumulate but `cycles_now()` always returns 0, so cycle fields in the
40/// snapshot will be 0. Host-side `Instant` timing in `telepath-client` remains
41/// meaningful for bench-pty.
42///
43/// # Safety
44///
45/// On ARM: uses `cortex_m::peripheral::Peripherals::steal()`. Only sound on
46/// single-core Cortex-M targets with `profile` intentionally enabled.
47pub fn init_dwt() {
48    #[cfg(target_arch = "arm")]
49    unsafe {
50        let mut cp = cortex_m::peripheral::Peripherals::steal();
51        cp.DCB.enable_trace();
52        cortex_m::peripheral::DWT::unlock();
53        cp.DWT.enable_cycle_counter();
54        cp.DWT.cyccnt.write(0);
55    }
56}
57
58/// Read the current DWT cycle counter value.
59/// Returns 0 on non-ARM targets (e.g. x86_64 host-pty-server).
60#[inline(always)]
61pub fn cycles_now() -> u32 {
62    #[cfg(target_arch = "arm")]
63    {
64        cortex_m::peripheral::DWT::cycle_count()
65    }
66    #[cfg(not(target_arch = "arm"))]
67    {
68        0
69    }
70}
71
72/// Return the current metrics snapshot and atomically reset all counters.
73pub fn snapshot_and_reset() -> MetricsSnapshot {
74    MetricsSnapshot {
75        encode_cycles: ENCODE_CYCLES.swap(0, Ordering::Relaxed),
76        decode_cycles: DECODE_CYCLES.swap(0, Ordering::Relaxed),
77        encoded_bytes: ENCODED_BYTES.swap(0, Ordering::Relaxed),
78        decoded_bytes: DECODED_BYTES.swap(0, Ordering::Relaxed),
79        sample_count: SAMPLE_COUNT.swap(0, Ordering::Relaxed),
80    }
81}
82
83// ---------------------------------------------------------------------------
84// CmdID 0xFFFE registration
85//
86// We cannot use the `#[command]` proc-macro inside `telepath-server` itself
87// because the macro emits `::telepath_server::...` paths that do not resolve
88// when compiling the crate itself. Instead we build the CommandMetadata and
89// the linkme slice entry by hand — equivalent to what the macro would emit.
90// TODO(#76): after rzCOBS upstream lands, ENCODE_CYCLES tracks rzcobs_encode
91// instead of cobs_encode. The metric semantics remain valid after the swap.
92// ---------------------------------------------------------------------------
93
94fn get_metrics_shim(
95    input: &[u8],
96    output: &mut [u8],
97    _resources: &crate::ResourceRegistry,
98) -> Result<crate::DispatchOutcome, crate::DispatchError> {
99    if !input.is_empty() {
100        return Err(crate::DispatchError::DeserializeError);
101    }
102    let snap = snapshot_and_reset();
103    postcard::to_slice(&snap, output)
104        .map(|s| crate::DispatchOutcome::Ok(s.len()))
105        .map_err(|_| crate::DispatchError::SerializeError)
106}
107
108fn get_metrics_args_schema(out: &mut [u8]) -> Result<usize, ()> {
109    postcard::to_slice(<() as crate::__postcard_schema::Schema>::SCHEMA, out)
110        .map(|s| s.len())
111        .map_err(|_| ())
112}
113
114fn get_metrics_ret_schema(out: &mut [u8]) -> Result<usize, ()> {
115    postcard::to_slice(
116        <MetricsSnapshot as crate::__postcard_schema::Schema>::SCHEMA,
117        out,
118    )
119    .map(|s| s.len())
120    .map_err(|_| ())
121}
122
123pub const GET_METRICS_CMD: crate::CommandMetadata = crate::CommandMetadata {
124    name: "get_metrics",
125    id: telepath_wire::CMD_ID_METRICS,
126    invoke: get_metrics_shim,
127    args_schema: get_metrics_args_schema,
128    ret_schema: get_metrics_ret_schema,
129    arg_names: "",
130};
131
132#[allow(non_upper_case_globals, non_snake_case)]
133#[crate::__linkme::distributed_slice(crate::TELEPATH_COMMANDS)]
134#[linkme(crate = crate::__linkme)]
135static __TELEPATH_REG_GET_METRICS: crate::CommandMetadata = GET_METRICS_CMD;