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<usize, 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| 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;